diff --git a/package.json b/package.json index 28c13e6f..cc8eab8f 100755 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "build": "tsc", "lint:fix": "npm run lint -- --fix", "lint": "eslint 'src/**/*.{ts,tsx}'", - "prepare": "yarn build && husky install", + "prepare": "yarn build && husky", "test": "jest" }, "files": [ diff --git a/src/node/node.ts b/src/node/node.ts index f0e0325b..c358d168 100755 --- a/src/node/node.ts +++ b/src/node/node.ts @@ -12,6 +12,8 @@ export enum NodeMetaKeys { export interface NodeMeta { childApplicability?: { [uuid: string]: boolean } + childrenMaxCount?: { [uuid: string]: number } + childrenMinCount?: { [uuid: string]: number } h?: string[] hCode?: string[] defaultValueApplied?: boolean diff --git a/src/node/nodes.ts b/src/node/nodes.ts index c5707c8d..e789e6a1 100755 --- a/src/node/nodes.ts +++ b/src/node/nodes.ts @@ -1,13 +1,47 @@ +import { NodeDef, NodeDefCountType, NodeDefs } from '../nodeDef' import { Dates, Objects } from '../utils' import { Node } from './node' const isRoot = (node: Node): boolean => !node.parentUuid + const areEqual = (nodeA: Node, nodeB: Node): boolean => nodeA.uuid === nodeB.uuid -const isChildApplicable = (node: Node, nodeDefUuid: string) => { + +const isChildApplicable = (node: Node, nodeDefUuid: string): boolean => { // if child applicability is not defined for a node definition, consider it applicable return node.meta?.childApplicability?.[nodeDefUuid] !== false } -const assocChildApplicability = (node: Node, nodeDefUuid: string, applicable: boolean) => { +const getChildrenCount = (params: { parentNode: Node; nodeDef: NodeDef; countType: NodeDefCountType }): number => { + const { parentNode, nodeDef, countType } = params + const countIndex = + countType === NodeDefCountType.max ? parentNode.meta?.childrenMaxCount : parentNode.meta?.childrenMinCount + + const count = countIndex?.[nodeDef.uuid] + if (!Objects.isEmpty(count)) return count! + + // count can be a constant value, specified in the node def min/max count prop + const nodeDefCount = + countType === NodeDefCountType.max ? NodeDefs.getMaxCount(nodeDef) : NodeDefs.getMinCount(nodeDef) + return nodeDefCount ? Number(nodeDefCount) : NaN +} + +const getChildrenMaxCount = (params: { parentNode: Node; nodeDef: NodeDef }): number => + getChildrenCount({ ...params, countType: NodeDefCountType.max }) + +const getChildrenMinCount = (params: { parentNode: Node; nodeDef: NodeDef }): number => + getChildrenCount({ ...params, countType: NodeDefCountType.min }) + +const getHierarchy = (node: Node): string[] => [...(node.meta?.h ?? [])] + +const getHierarchyCode = (node: Node): string[] => [...(node.meta?.hCode ?? [])] + +const mergeNodes = (target: Node, ...sources: Node[] | object[]): Node => + Objects.deepMerge(target, ...sources) as unknown as Node + +const isDefaultValueApplied = (node: Node): boolean => node?.meta?.defaultValueApplied ?? false + +const isValueBlank = (node: Node): boolean => Objects.isEmpty(node.value) + +const assocChildApplicability = (node: Node, nodeDefUuid: string, applicable: boolean): Node => { const childApplicability = { ...(node.meta?.childApplicability ?? {}) } if (!applicable) { childApplicability[nodeDefUuid] = applicable @@ -21,6 +55,7 @@ const assocChildApplicability = (node: Node, nodeDefUuid: string, applicable: bo dateModified: Dates.nowFormattedForStorage(), } } + const dissocChildApplicability = (node: Node, nodeDefUuid: string) => { const childApplicability = { ...(node.meta?.childApplicability ?? {}) } delete childApplicability[nodeDefUuid] @@ -29,16 +64,41 @@ const dissocChildApplicability = (node: Node, nodeDefUuid: string) => { meta: { ...node.meta, childApplicability }, } } -const getHierarchy = (node: Node) => [...(node.meta?.h ?? [])] -const getHierarchyCode = (node: Node) => [...(node.meta?.hCode ?? [])] - -const mergeNodes = (target: Node, ...sources: Node[] | object[]): Node => - Objects.deepMerge(target, ...sources) as unknown as Node +const assocChildrenCount = (params: { + node: Node + nodeDefUuid: string + count: number + countType: NodeDefCountType +}): Node => { + const { node, nodeDefUuid, count, countType } = params + const countIndex = { + ...((countType === NodeDefCountType.max ? node.meta?.childrenMaxCount : node.meta?.childrenMinCount) ?? {}), + } + if (isNaN(count)) { + delete countIndex[nodeDefUuid] + } else { + countIndex[nodeDefUuid] = count + } + const metaUpdated = { ...node.meta } + if (countType === NodeDefCountType.max) { + metaUpdated.childrenMaxCount = countIndex + } else { + metaUpdated.childrenMinCount = countIndex + } + return { + ...node, + meta: metaUpdated, + updated: true, + dateModified: Dates.nowFormattedForStorage(), + } +} -const isDefaultValueApplied = (node: Node): boolean => node?.meta?.defaultValueApplied ?? false +const assocChildrenMaxCount = (params: { node: Node; nodeDefUuid: string; count: number }): Node => + assocChildrenCount({ ...params, countType: NodeDefCountType.max }) -const isValueBlank = (node: Node): boolean => Objects.isEmpty(node.value) +const assocChildrenMinCount = (params: { node: Node; nodeDefUuid: string; count: number }): Node => + assocChildrenCount({ ...params, countType: NodeDefCountType.min }) const removeStatusFlags = (node: Node): Node => { delete node['created'] @@ -51,12 +111,19 @@ export const Nodes = { isRoot, areEqual, isChildApplicable, - assocChildApplicability, - dissocChildApplicability, - mergeNodes, + getChildrenCount, + getChildrenMaxCount, + getChildrenMinCount, getHierarchy, getHierarchyCode, isDefaultValueApplied, isValueBlank, + // update + assocChildApplicability, + dissocChildApplicability, + assocChildrenCount, + assocChildrenMaxCount, + assocChildrenMinCount, + mergeNodes, removeStatusFlags, } diff --git a/src/nodeDef/index.ts b/src/nodeDef/index.ts index dcc0d3fe..3f6aed10 100755 --- a/src/nodeDef/index.ts +++ b/src/nodeDef/index.ts @@ -11,7 +11,7 @@ export type { NodeDefValidations, NodeDefMap, } from './nodeDef' -export { NodeDefType } from './nodeDef' +export { NodeDefCountType, NodeDefType } from './nodeDef' export { NodeDefs } from './nodeDefs' export type { NodeDefService } from './service' diff --git a/src/nodeDef/nodeDef.ts b/src/nodeDef/nodeDef.ts index 63c8cc98..3f52dc60 100755 --- a/src/nodeDef/nodeDef.ts +++ b/src/nodeDef/nodeDef.ts @@ -1,4 +1,4 @@ -import { UUIDs } from '../utils' +import { Objects, UUIDs } from '../utils' import { ArenaObject, Factory } from '../common' import { Labels } from '../language' import { ValidationSeverity } from '../validation' @@ -20,12 +20,17 @@ export enum NodeDefType { formHeader = 'formHeader', } +export enum NodeDefCountType { + max = 'max', + min = 'min', +} + export interface NodeDefMeta { - h: Array + h: string[] } export interface NodeDefProps { - cycles?: Array + cycles?: string[] descriptions?: Labels key?: boolean autoIncrementalKey?: boolean @@ -70,29 +75,32 @@ export interface NodeDefExpressionFactoryParams { export const NodeDefExpressionFactory: Factory = { createInstance: (params: NodeDefExpressionFactoryParams): NodeDefExpression => { - const { applyIf, expression, severity } = params - return { applyIf, expression, severity, uuid: UUIDs.v4() } + const result = { uuid: UUIDs.v4() } + Object.assign(result, Objects.deleteEmptyProps({ ...params })) + return result }, } +export type NodeDefCountExpression = string | NodeDefExpression[] + export interface NodeDefCountValidations { - max?: number - min?: number + max?: NodeDefCountExpression + min?: NodeDefCountExpression } export interface NodeDefValidations { count?: NodeDefCountValidations - expressions?: Array + expressions?: NodeDefExpression[] required?: boolean unique?: boolean } export interface NodeDefPropsAdvanced { - applicable?: Array - defaultValues?: Array + applicable?: NodeDefExpression[] + defaultValues?: NodeDefExpression[] defaultValueEvaluatedOneTime?: boolean excludedInClone?: boolean - formula?: Array + formula?: NodeDefExpression[] validations?: NodeDefValidations // file attribute fileNameExpression?: string diff --git a/src/nodeDef/nodeDefs.ts b/src/nodeDef/nodeDefs.ts index 23ce2bfa..c7114020 100644 --- a/src/nodeDef/nodeDefs.ts +++ b/src/nodeDef/nodeDefs.ts @@ -1,9 +1,11 @@ import { LanguageCode } from '../language' import { valuePropsCoordinate } from '../node/nodeValueProps' import { defaultCycle } from '../survey' -import { Numbers, Objects, Strings } from '../utils' +import { Objects, Strings } from '../utils' import { NodeDef, + NodeDefCountExpression, + NodeDefCountType, NodeDefExpression, NodeDefLayout, NodeDefProps, @@ -128,15 +130,27 @@ const getItemsFilter = (nodeDef: NodeDef): string | undefined => nodeDef.pr const getValidations = (nodeDef: NodeDef): NodeDefValidations | undefined => nodeDef.propsAdvanced?.validations +const getValidationsExpressions = (nodeDef: NodeDef): NodeDefExpression[] => + getValidations(nodeDef)?.expressions ?? [] + const isRequired = (nodeDef: NodeDef): boolean => getValidations(nodeDef)?.required ?? false -// // Min max -const getMinCount = (nodeDef: NodeDef) => Numbers.toNumber(getValidations(nodeDef)?.count?.min) +// // Min/max count + +const getCount = (nodeDef: NodeDef, countType: NodeDefCountType): NodeDefCountExpression | undefined => { + const countValidations = getValidations(nodeDef)?.count + if (!countValidations) return undefined + return countType === NodeDefCountType.max ? countValidations.max : countValidations.min +} + +const getMinCount = (nodeDef: NodeDef): NodeDefCountExpression | undefined => + getCount(nodeDef, NodeDefCountType.min) -const getMaxCount = (nodeDef: NodeDef) => Numbers.toNumber(getValidations(nodeDef)?.count?.max) +const getMaxCount = (nodeDef: NodeDef): NodeDefCountExpression | undefined => + getCount(nodeDef, NodeDefCountType.max) -const hasMinOrMaxCount = (nodeDef: NodeDef) => - !Number.isNaN(getMinCount(nodeDef)) || !Number.isNaN(getMaxCount(nodeDef)) +const hasMinOrMaxCount = (nodeDef: NodeDef): boolean => + !Objects.isEmpty(getMinCount(nodeDef)) || !Objects.isEmpty(getMaxCount(nodeDef)) // layout const getLayoutProps = @@ -262,9 +276,11 @@ export const NodeDefs = { isCodeShown, // validations getValidations, + getValidationsExpressions, isRequired, // // Min Max hasMinOrMaxCount, + getCount, getMaxCount, getMinCount, // Analysis diff --git a/src/record/_records/recordGetters.ts b/src/record/_records/recordGetters.ts index 1a32a6d8..fbe44d5d 100755 --- a/src/record/_records/recordGetters.ts +++ b/src/record/_records/recordGetters.ts @@ -330,7 +330,6 @@ export const findEntityByKeyValues = (params: { return siblingEntities.find((siblingEntity) => { const siblingEntityKeyValuesByDefUuid = getEntityKeyValuesByDefUuid({ survey, - cycle, record, entity: siblingEntity, keyDefs, diff --git a/src/record/recordNodesUpdater/recordNodeDependentsCountUpdater.ts b/src/record/recordNodesUpdater/recordNodeDependentsCountUpdater.ts new file mode 100644 index 00000000..6832d35d --- /dev/null +++ b/src/record/recordNodesUpdater/recordNodeDependentsCountUpdater.ts @@ -0,0 +1,73 @@ +import { User } from '../../auth' +import { NodeDefCountType, NodeDefs } from '../../nodeDef' +import { Record } from '../record' +import { Survey, SurveyDependencyType } from '../../survey' +import { Node, NodePointer, Nodes } from '../../node' +import { RecordUpdateResult } from './recordUpdateResult' +import { Records } from '../records' +import { RecordExpressionEvaluator } from '../recordExpressionEvaluator' +import { Objects } from '../../utils' + +export const updateDependentsCount = (params: { + user: User + survey: Survey + record: Record + node: Node + countType: NodeDefCountType + sideEffect?: boolean + timezoneOffset?: number +}): RecordUpdateResult => { + const { user, survey, record, node, countType, timezoneOffset, sideEffect = false } = params + + const updateResult = new RecordUpdateResult({ record }) + + // 1. fetch dependent nodes + const dependencyType = + countType === NodeDefCountType.max ? SurveyDependencyType.maxCount : SurveyDependencyType.minCount + + const nodePointersToUpdate = Records.getDependentNodePointers({ + survey, + record, + node, + dependencyType, + }) + + // 2. update expr to node and dependent nodes + // NOTE: don't do it in parallel, same nodeCtx metadata could be overwritten + const expressionEvaluator = new RecordExpressionEvaluator() + nodePointersToUpdate.forEach((nodePointer: NodePointer) => { + const { nodeCtx: nodeCtxNodePointer, nodeDef: nodeDefNodePointer } = nodePointer + + const expressionsToEvaluate = NodeDefs.getCount(nodeDefNodePointer, countType) + if (Objects.isEmpty(expressionsToEvaluate) || !Array.isArray(expressionsToEvaluate)) return + + // 3. evaluate applicable expression + const nodeCtxUuid = nodeCtxNodePointer.uuid + // nodeCtx could have been updated in a previous iteration + const nodeCtx = updateResult.getNodeByUuid(nodeCtxUuid) ?? nodeCtxNodePointer + + const countResult = expressionEvaluator.evalApplicableExpression({ + user, + survey, + record: updateResult.record, + nodeCtx, + expressions: expressionsToEvaluate, + timezoneOffset, + }) + + const count = Number(countResult?.value) + + // 4. persist updated count if changed, and return updated nodes + const nodeDefUuid = nodeDefNodePointer.uuid + + if (Nodes.getChildrenCount({ parentNode: nodeCtx, nodeDef: nodeDefNodePointer, countType }) !== count) { + // count changed + + // 5. update node and add it to nodes updated + const nodeCtxUpdated = Nodes.assocChildrenCount({ node: nodeCtx, nodeDefUuid, count, countType }) + updateResult.addNode(nodeCtxUpdated, { sideEffect }) + } + }) + + return updateResult +} diff --git a/src/record/recordNodesUpdater/recordNodesCreator.ts b/src/record/recordNodesUpdater/recordNodesCreator.ts index edb90fd1..d9b2ceac 100644 --- a/src/record/recordNodesUpdater/recordNodesCreator.ts +++ b/src/record/recordNodesUpdater/recordNodesCreator.ts @@ -1,6 +1,6 @@ import { CategoryItem } from '../../category' import { SystemError } from '../../error' -import { Node, NodeFactory } from '../../node' +import { Node, NodeFactory, Nodes } from '../../node' import { NodeValues } from '../../node/nodeValues' import { NodeDef, NodeDefCode, NodeDefEntity, NodeDefType, NodeDefs } from '../../nodeDef' import { Survey } from '../../survey' @@ -25,10 +25,11 @@ export type NodeCreateParams = NodesUpdateParams & { createMultipleEntities?: boolean } -const getNodesToInsertCount = (nodeDef: NodeDef): number => { - if (NodeDefs.isSingle(nodeDef)) return 1 +const getNodesToInsertCount = (params: { parentNode: Node | undefined; nodeDef: NodeDef }): number => { + const { nodeDef, parentNode } = params + if (!parentNode || NodeDefs.isSingle(nodeDef)) return 1 if (nodeDef.type === NodeDefType.code) return 0 // never create nodes for multiple code attributes - return NodeDefs.getMinCount(nodeDef) ?? 0 + return Nodes.getChildrenMinCount({ parentNode, nodeDef }) ?? 0 } const getEnumeratingCategoryItems = (params: { survey: Survey; enumerator: NodeDefCode }): CategoryItem[] => { @@ -96,7 +97,7 @@ const createChildNodesBasedOnMinCount = (params: NodeCreateParams & { updateResu } } - const nodesToInsertCount = getNodesToInsertCount(nodeDef) + const nodesToInsertCount = getNodesToInsertCount({ parentNode, nodeDef }) if (nodesToInsertCount === 0 || (!createMultipleEntities && NodeDefs.isMultipleEntity(nodeDef))) { return // do nothing } diff --git a/src/record/recordNodesUpdater/recordNodesUpdater.ts b/src/record/recordNodesUpdater/recordNodesUpdater.ts index 5a6ecc93..c8c5f26a 100644 --- a/src/record/recordNodesUpdater/recordNodesUpdater.ts +++ b/src/record/recordNodesUpdater/recordNodesUpdater.ts @@ -3,11 +3,13 @@ import { Queue } from '../../utils' import * as DependentDefaultValuesUpdater from './recordNodeDependentsDefaultValuesUpdater' import * as DependentApplicableUpdater from './recordNodeDependentsApplicableUpdater' import * as DependentCodeAttributesUpdater from './recordNodeDependentsCodeAttributesUpdater' +import * as DependentCountUpdater from './recordNodeDependentsCountUpdater' import * as DependentFileNamesUpdater from './recordNodeDependentsFileNamesEvaluator' import { Survey } from '../../survey' import { Record } from '../record' import { Node } from '../../node' import { RecordUpdateResult } from './recordUpdateResult' +import { NodeDefCountType } from '../../nodeDef' import { User } from '../../auth' import { Dictionary } from '../../common' @@ -35,7 +37,7 @@ export const updateNodesDependents = ( const createEvaluationContext = (node: Node): ExpressionEvaluationContext & { node: Node } => ({ user, survey, - record: updateResult.record, + record: updateResult.record, // updateResult.record changes at every step (when sideEffect=false) timezoneOffset, sideEffect, node, @@ -53,19 +55,33 @@ export const updateNodesDependents = ( const visitedCount = visitedCountByUuid[nodeUuid] ?? 0 if (visitedCount < MAX_DEPENDENTS_VISITING_TIMES) { - // Update dependents (applicability) + // min count + const minCountUpdateResult = DependentCountUpdater.updateDependentsCount({ + ...createEvaluationContext(node), + countType: NodeDefCountType.min, + }) + updateResult.merge(minCountUpdateResult) + + // max count + const maxCountUpdateResult = DependentCountUpdater.updateDependentsCount({ + ...createEvaluationContext(node), + countType: NodeDefCountType.max, + }) + updateResult.merge(maxCountUpdateResult) + + // applicability const applicabilityUpdateResult = DependentApplicableUpdater.updateSelfAndDependentsApplicable( createEvaluationContext(node) ) updateResult.merge(applicabilityUpdateResult) - // Update dependents (default values) + // default values const defaultValuesUpdateResult = DependentDefaultValuesUpdater.updateSelfAndDependentsDefaultValues( createEvaluationContext(node) ) updateResult.merge(defaultValuesUpdateResult) - // Update dependents (code attributes) + // code attributes const dependentCodeAttributesUpdateResult = DependentCodeAttributesUpdater.updateDependentCodeAttributes( createEvaluationContext(node) ) @@ -78,6 +94,8 @@ export const updateNodesDependents = ( updateResult.merge(dependentFileNamesUpdateResult) const nodesUpdatedCurrent: Dictionary = { + ...minCountUpdateResult.nodes, + ...maxCountUpdateResult.nodes, ...applicabilityUpdateResult.nodes, ...defaultValuesUpdateResult.nodes, ...dependentCodeAttributesUpdateResult.nodes, diff --git a/src/record/recordUpdater.nodeDelete.test.ts b/src/record/recordUpdater.nodeDelete.test.ts index 1fbe8dfe..5ac68ae8 100644 --- a/src/record/recordUpdater.nodeDelete.test.ts +++ b/src/record/recordUpdater.nodeDelete.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { SurveyBuilder, SurveyObjectBuilders } from '../tests/builder/surveyBuilder' import { RecordBuilder, RecordNodeBuilders } from '../tests/builder/recordBuilder' @@ -73,7 +72,7 @@ describe('RecordUpdater - node delete', () => { integerDef('identifier').key(), entityDef('mult_entity', integerDef('mult_entity_id').key(), integerDef('mult_entity_attr')) .multiple() - .minCount(2), + .minCount('2'), integerDef('mult_entity_count').readOnly().defaultValue('count(mult_entity)'), integerDef('mult_entity_attr_sum').readOnly().defaultValue('sum(mult_entity.mult_entity_attr)') ) diff --git a/src/record/recordValidator/countVaildator.ts b/src/record/recordValidator/countVaildator.ts index d32ae568..00397b41 100644 --- a/src/record/recordValidator/countVaildator.ts +++ b/src/record/recordValidator/countVaildator.ts @@ -40,13 +40,14 @@ const _createValidationResult = (params: { } const validateChildrenCount = (params: { + parentNode: Node nodeDefChild: NodeDef count: number }): Validation => { - const { nodeDefChild, count } = params + const { parentNode, nodeDefChild, count } = params - const minCount = NodeDefs.getMinCount(nodeDefChild) - const maxCount = NodeDefs.getMaxCount(nodeDefChild) + const minCount = Nodes.getChildrenMinCount({ parentNode, nodeDef: nodeDefChild }) + const maxCount = Nodes.getChildrenMaxCount({ parentNode, nodeDef: nodeDefChild }) const minCountValid = Number.isNaN(minCount) || count >= minCount const maxCountValid = Number.isNaN(maxCount) || count <= maxCount @@ -84,7 +85,7 @@ const _validateNodePointer = (params: { if (Nodes.isChildApplicable(nodeCtx, nodeDef.uuid)) { if (NodeDefs.hasMinOrMaxCount(nodeDef)) { const count = _countChildren({ record, parentNode: nodeCtx, childDef: nodeDef }) - return validateChildrenCount({ nodeDefChild: nodeDef, count }) + return validateChildrenCount({ parentNode: nodeCtx, nodeDefChild: nodeDef, count }) } } return ValidationFactory.createInstance() @@ -127,18 +128,15 @@ const validateChildrenCountNodes = (params: { const nodePointersToValidate = _getNodePointersToValidate({ survey, record, node }) nodePointersToValidate.forEach((nodePointer) => { + const { nodeCtx, nodeDef } = nodePointer // check if validated already const validationChildrenCountKey = RecordValidations.getValidationChildrenCountKey({ - nodeParentUuid: nodePointer.nodeCtx.uuid, - nodeDefChildUuid: nodePointer.nodeDef.uuid, + nodeParentUuid: nodeCtx.uuid, + nodeDefChildUuid: nodeDef.uuid, }) if (!(validationChildrenCountKey in validationsAcc)) { // validate the children count of this node pointer - const validationNodePointer = _validateNodePointer({ - record, - nodeCtx: nodePointer.nodeCtx, - nodeDef: nodePointer.nodeDef, - }) + const validationNodePointer = _validateNodePointer({ record, nodeCtx, nodeDef }) validationsAcc[validationChildrenCountKey] = validationNodePointer } }) diff --git a/src/survey/survey.ts b/src/survey/survey.ts index 5eff1218..b934c637 100755 --- a/src/survey/survey.ts +++ b/src/survey/survey.ts @@ -19,6 +19,8 @@ export enum SurveyDependencyType { defaultValues = 'defaultValues', fileName = 'fileName', formula = 'formula', + maxCount = 'maxCount', + minCount = 'minCount', validations = 'validations', } diff --git a/src/survey/surveys/dependencies.ts b/src/survey/surveys/dependencies.ts index 6322771f..ed2b37aa 100644 --- a/src/survey/surveys/dependencies.ts +++ b/src/survey/surveys/dependencies.ts @@ -11,6 +11,8 @@ const isContextParentByDependencyType = { [SurveyDependencyType.defaultValues]: true, [SurveyDependencyType.fileName]: true, [SurveyDependencyType.formula]: false, + [SurveyDependencyType.maxCount]: true, + [SurveyDependencyType.minCount]: true, [SurveyDependencyType.validations]: true, } @@ -19,6 +21,8 @@ const selfReferenceAllowedByDependencyType = { [SurveyDependencyType.defaultValues]: false, [SurveyDependencyType.fileName]: false, [SurveyDependencyType.formula]: false, + [SurveyDependencyType.maxCount]: false, + [SurveyDependencyType.minCount]: false, [SurveyDependencyType.validations]: true, } @@ -27,6 +31,8 @@ const newDependecyGraph = () => ({ [SurveyDependencyType.defaultValues]: {}, [SurveyDependencyType.fileName]: {}, [SurveyDependencyType.formula]: {}, + [SurveyDependencyType.maxCount]: {}, + [SurveyDependencyType.minCount]: {}, [SurveyDependencyType.validations]: {}, }) @@ -155,10 +161,15 @@ export const addNodeDefDependencies = (params: { }) graphsUpdated = _addDependencies(SurveyDependencyType.defaultValues, NodeDefs.getDefaultValues(nodeDef)) graphsUpdated = _addDependencies(SurveyDependencyType.applicable, NodeDefs.getApplicable(nodeDef)) - graphsUpdated = _addDependencies( - SurveyDependencyType.validations, - NodeDefs.getValidations(nodeDef)?.expressions ?? [] - ) + graphsUpdated = _addDependencies(SurveyDependencyType.validations, NodeDefs.getValidationsExpressions(nodeDef)) + const maxCount = NodeDefs.getMaxCount(nodeDef) + if (Array.isArray(maxCount)) { + graphsUpdated = _addDependencies(SurveyDependencyType.maxCount, maxCount) + } + const minCount = NodeDefs.getMinCount(nodeDef) + if (Array.isArray(minCount)) { + graphsUpdated = _addDependencies(SurveyDependencyType.minCount, minCount) + } // file name expression if (NodeDefs.getType(nodeDef) === NodeDefType.file) { const fileNameExpression = NodeDefs.getFileNameExpression(nodeDef as NodeDefFile) diff --git a/src/survey/surveys/index.ts b/src/survey/surveys/index.ts index 96d30cc1..8e863b15 100755 --- a/src/survey/surveys/index.ts +++ b/src/survey/surveys/index.ts @@ -19,6 +19,7 @@ import { getNodeDefSource, isNodeDefAncestor, getNodeDefKeys, + getRootKeys, getNodeDefsIncludedInMultipleEntitySummary, getNodeDefParentCode, getNodeDefAncestorCodes, @@ -98,6 +99,7 @@ export const Surveys = { getNodeDefSource, isNodeDefAncestor, getNodeDefKeys, + getRootKeys, getNodeDefsIncludedInMultipleEntitySummary, isNodeDefEnumerator, getNodeDefEnumerator, diff --git a/src/survey/surveys/nodeDefs.ts b/src/survey/surveys/nodeDefs.ts index db9000f4..da9e6c1e 100755 --- a/src/survey/surveys/nodeDefs.ts +++ b/src/survey/surveys/nodeDefs.ts @@ -399,6 +399,12 @@ export const getNodeDefKeys = (params: { }) } +export const getRootKeys = (params: { survey: Survey; cycle?: string }) => { + const { survey, cycle } = params + const rootDef = getNodeDefRoot({ survey }) + return getNodeDefKeys({ survey, nodeDef: rootDef, cycle }) +} + export const getNodeDefEnumerator = (params: { survey: Survey; entityDef: NodeDefEntity }): NodeDefCode | undefined => { const { survey, entityDef } = params if (!entityDef.props.enumerate) return undefined diff --git a/src/tests/builder/surveyBuilder/nodeDefBuilder.ts b/src/tests/builder/surveyBuilder/nodeDefBuilder.ts index be63d06c..fefed689 100755 --- a/src/tests/builder/surveyBuilder/nodeDefBuilder.ts +++ b/src/tests/builder/surveyBuilder/nodeDefBuilder.ts @@ -46,11 +46,20 @@ export abstract class NodeDefBuilder { return this } - minCount(count: number): this { + maxCount(countExpr: string): this { + return Objects.assocPath({ + obj: this, + path: ['propsAdvanced', 'validations', 'count', 'max'], + value: countExpr, + sideEffect: true, + }) + } + + minCount(countExpr: string): this { return Objects.assocPath({ obj: this, path: ['propsAdvanced', 'validations', 'count', 'min'], - value: count, + value: countExpr, sideEffect: true, }) } diff --git a/src/utils/_objects/deleteEmptyProps.ts b/src/utils/_objects/deleteEmptyProps.ts new file mode 100644 index 00000000..90fd184a --- /dev/null +++ b/src/utils/_objects/deleteEmptyProps.ts @@ -0,0 +1,10 @@ +import { isEmpty } from './isEmpty' + +export const deleteEmptyProps = (obj: any): any => { + Object.entries(obj ?? {}).forEach(([key, value]) => { + if (isEmpty(value)) { + delete obj[key] + } + }) + return obj +} diff --git a/src/utils/_objects/index.ts b/src/utils/_objects/index.ts index fa3f04b2..6a5d9d97 100644 --- a/src/utils/_objects/index.ts +++ b/src/utils/_objects/index.ts @@ -3,6 +3,7 @@ import { assocPath } from './assocPath' import { camelize } from './camelize' import { deepMerge } from './deepMerge' import { difference } from './difference' +import { deleteEmptyProps } from './deleteEmptyProps' import { dissoc } from './dissoc' import { dissocPath } from './dissocPath' import { dissocPathIfEmpty } from './dissocPathIfEmpty' @@ -23,6 +24,7 @@ export const Objects = { assocPath, camelize, deepMerge, + deleteEmptyProps, difference, dissoc, dissocPath, diff --git a/src/validation/factory.ts b/src/validation/factory.ts index 3ccf7c83..df5cdcdd 100755 --- a/src/validation/factory.ts +++ b/src/validation/factory.ts @@ -1,23 +1,18 @@ -import { Factory } from '../common' +import { Dictionary, Factory } from '../common' import { Labels } from '../language' import { Validation, ValidationResult, ValidationSeverity } from './validation' type ValidationFactoryParams = { - errors?: Array - fields?: { - [name: string]: Validation - } + errors?: ValidationResult[] + fields?: Dictionary valid?: boolean - warnings?: Array + warnings?: ValidationResult[] } export const ValidationFactory: Factory = { createInstance: (params?: ValidationFactoryParams): Validation => { const defaultParams = { valid: true, - errors: new Array(), - warnings: new Array(), - fields: {}, } const { errors, fields, valid, warnings } = { ...defaultParams, @@ -35,7 +30,7 @@ export const ValidationFactory: Factory = { type ValidationResultFactoryParams = { messages?: Labels key?: string - params?: { [key: string]: any } + params?: Dictionary severity?: ValidationSeverity valid?: boolean } @@ -45,8 +40,8 @@ export const ValidationResultFactory: Factory { const fieldsWithValidationRecalculated: ValidationFields = {} - Object.entries(validation.fields).forEach(([fieldKey, fieldValidation]) => { + Object.entries(validation.fields ?? {}).forEach(([fieldKey, fieldValidation]) => { const fieldValidationUpdated = recalculateValidity(fieldValidation) fieldsWithValidationRecalculated[fieldKey] = fieldValidationUpdated if (!fieldValidationUpdated.valid) {