Skip to content

Commit

Permalink
feat(form-logic): introduce form logic model validation (#2302)
Browse files Browse the repository at this point in the history
  • Loading branch information
yong-jie authored Jul 14, 2021
1 parent c3a329b commit 8bf6a08
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 155 deletions.
50 changes: 50 additions & 0 deletions src/app/models/__tests__/form.server.model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,56 @@ describe('Form Model', () => {
expect(Object.keys(saved)).not.toContain('extra')
})

it('should create and save successfully with form_logics that reference nonexistent form_fields', async () => {
// Arrange
const FORM_LOGICS = {
form_logics: [
{
conditions: [
{
_id: '',
field: new ObjectId(),
state: 'is equals to',
value: '',
ifValueType: 'number',
},
],
logicType: 'preventSubmit',
preventSubmitMessage: '',
},
],
}
const formParamsWithLogic = merge({}, MOCK_FORM_PARAMS, FORM_LOGICS)

// Act
const validForm = new Form(formParamsWithLogic)
const saved = await validForm.save()

// Assert
// All fields should exist
// Object Id should be defined when successfully saved to MongoDB.
expect(saved._id).toBeDefined()
expect(saved.created).toBeInstanceOf(Date)
expect(saved.lastModified).toBeInstanceOf(Date)
// Retrieve object and compare to params, remove indeterministic keys
const actualSavedObject = omit(saved.toObject(), [
'_id',
'created',
'lastModified',
'__v',
])
actualSavedObject.form_logics = actualSavedObject.form_logics?.map(
(logic) => omit(logic, '_id'),
)
const expectedObject = merge(
{},
FORM_DEFAULTS,
MOCK_FORM_PARAMS,
FORM_LOGICS,
)
expect(actualSavedObject).toEqual(expectedObject)
})

it('should create and save successfully with valid permissionList emails', async () => {
// Arrange
// permissionList has email with valid domain
Expand Down
102 changes: 81 additions & 21 deletions src/app/models/form.server.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import BSON from 'bson-ext'
import BSON, { ObjectId } from 'bson-ext'
import { compact, omit, pick, uniq } from 'lodash'
import mongoose, {
Mongoose,
Expand All @@ -11,6 +11,7 @@ import validator from 'validator'

import { MB } from '../../shared/constants'
import { reorder } from '../../shared/util/immutable-array-fns'
import { getApplicableIfStates } from '../../shared/util/logic'
import {
AuthType,
BasicField,
Expand All @@ -30,7 +31,9 @@ import {
IFormDocument,
IFormModel,
IFormSchema,
ILogicSchema,
IPopulatedForm,
LogicConditionState,
LogicDto,
LogicType,
Permission,
Expand Down Expand Up @@ -208,7 +211,67 @@ const compileFormModel = (db: Mongoose): IFormModel => {
'Check that your form is MyInfo-authenticated, is an email mode form and has 30 or fewer MyInfo fields.',
},
},
form_logics: [LogicSchema],
form_logics: {
type: [LogicSchema],
validate: {
validator(this: IFormSchema, v: ILogicSchema[]) {
/**
* A validatable condition is incomplete if there is a possibility
* that its fieldType is null, which is a sign that a condition's
* field property references a non-existent form_field.
*/
type IncompleteValidatableCondition = {
state: LogicConditionState
fieldType?: BasicField
}

/**
* A condition object is said to be validatable if it contains the two
* necessary for validation: fieldType and state
*/
type ValidatableCondition = IncompleteValidatableCondition & {
fieldType: BasicField
}

const isConditionReferencesExistingField = (
condition: IncompleteValidatableCondition,
): condition is ValidatableCondition => !!condition.fieldType

const conditions = v.flatMap((logic) => {
return logic.conditions.map<IncompleteValidatableCondition>(
(condition) => {
const {
field,
state,
}: { field: ObjectId | string; state: LogicConditionState } =
condition
return {
state,
fieldType: this.form_fields?.find(
(f: IFieldSchema) => String(f._id) === String(field),
)?.fieldType,
}
},
)
})

return conditions.every((condition) => {
/**
* Form fields can get deleted by form admins, which causes logic
* conditions to reference invalid fields. Here we bypass validation
* and allow these conditions to be saved, so we don't make life
* difficult for form admins.
*/
if (!isConditionReferencesExistingField(condition)) return true

const { fieldType, state } = condition
const applicableIfStates = getApplicableIfStates(fieldType)
return applicableIfStates.includes(state)
})
},
message: 'Form logic condition validation failed.',
},
},

admin: {
type: Schema.Types.ObjectId,
Expand Down Expand Up @@ -690,14 +753,13 @@ const compileFormModel = (db: Mongoose): IFormModel => {
formId: string,
createLogicBody: LogicDto,
): Promise<IFormSchema | null> {
return this.findByIdAndUpdate(
formId,
{ $push: { form_logics: createLogicBody } },
{
new: true,
runValidators: true,
},
).exec()
const form = await this.findById(formId).exec()
if (!form?.form_logics) return null
const newLogic = (
form.form_logics as Types.DocumentArray<ILogicSchema>
).create(createLogicBody)
form.form_logics.push(newLogic)
return form.save()
}

// Deletes specified form field by id.
Expand All @@ -718,17 +780,15 @@ const compileFormModel = (db: Mongoose): IFormModel => {
logicId: string,
updatedLogic: LogicDto,
): Promise<IFormSchema | null> {
return this.findByIdAndUpdate(
formId,
{
$set: { 'form_logics.$[object]': updatedLogic },
},
{
arrayFilters: [{ 'object._id': logicId }],
new: true,
runValidators: true,
},
).exec()
let form = await this.findById(formId).exec()
if (!form?.form_logics) return null
const index = form.form_logics.findIndex(
(logic) => String(logic._id) === logicId,
)
form = form.set(`form_logics.${index}`, updatedLogic, {
new: true,
})
return form.save()
}

FormSchema.statics.updateEndPageById = async function (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const { range } = require('lodash')
const { LogicType } = require('../../../../../types')
const FormLogic = require('../../services/form-logic/form-logic.client.service')
const FormLogic = require('../../../../../shared/util/logic')
const UpdateFormService = require('../../../../services/UpdateFormService')

angular
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit 8bf6a08

Please sign in to comment.