diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 1539d6b7650..609c88144fe 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/toolkit", - "version": "0.0.22-scopes", + "version": "0.0.28", "description": "OpenCRVS toolkit for building country configurations", "license": "MPL-2.0", "exports": { diff --git a/packages/toolkit/src/conditionals/index.ts b/packages/toolkit/src/conditionals/index.ts index 936f0ba0722..d1b219ccfee 100644 --- a/packages/toolkit/src/conditionals/index.ts +++ b/packages/toolkit/src/conditionals/index.ts @@ -8,6 +8,7 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ + import { JSONSchema } from '@opencrvs/commons/conditionals' import { ActionDocument } from '@opencrvs/commons/events' @@ -94,118 +95,205 @@ export function eventHasAction(type: ActionDocument['type']) { } } +export type FieldAPI = { + inArray: (values: string[]) => FieldAPI + isBeforeNow: () => FieldAPI + isEqualTo: (value: string) => FieldAPI + isUndefined: () => FieldAPI + not: { + inArray: (values: string[]) => FieldAPI + equalTo: (value: string) => FieldAPI + } + /** + * joins multiple conditions with OR instead of AND. + * @example field('fieldId').or((field) => field.isUndefined().not.inArray(['value1', 'value2'])).apply() + */ + or: (callback: (field: FieldAPI) => FieldAPI) => FieldAPI + /** + * @private + * @returns array of conditions. Used internally by methods that consolidate multiple conditions into one. + */ + _apply: () => JSONSchema[] + /** + * @public + * @returns single object for consolidated conditions + */ + apply: () => JSONSchema +} + +/** + * Generate conditional rules for a field. + * @param fieldId - The field ID conditions are being applied to + * + * @returns @see FieldAPI + */ export function field(fieldId: string) { - return { - isBeforeNow: () => ({ - type: 'object', - properties: { - $form: { - type: 'object', - properties: { - [fieldId]: { - type: 'string', - format: 'date', - // https://ajv.js.org/packages/ajv-formats.html#keywords-to-compare-values-formatmaximum-formatminimum-and-formatexclusivemaximum-formatexclusiveminimum - formatMaximum: { $data: '2/$now' } - } + const conditions: JSONSchema[] = [] + + const addCondition = (rule: JSONSchema) => { + conditions.push(rule) + return api + } + + const api: FieldAPI = { + isBeforeNow: () => + addCondition({ + type: 'object', + properties: { + $form: { + type: 'object', + properties: { + [fieldId]: { + type: 'string', + format: 'date', + formatMaximum: { $data: '2/$now' } + } + }, + required: [fieldId] }, - required: [fieldId] + $now: { + type: 'string', + format: 'date' + } }, - $now: { - type: 'string', - format: 'date' - } - }, - required: ['$form', '$now'] - }), - isEqualTo: (value: string) => ({ - type: 'object', - properties: { - $form: { - type: 'object', - properties: { - [fieldId]: { - const: value + required: ['$form', '$now'] + }), + isEqualTo: (value: string) => + addCondition({ + type: 'object', + properties: { + $form: { + type: 'object', + properties: { + [fieldId]: { + const: value + } + }, + required: [fieldId] + } + }, + required: ['$form'] + }), + isUndefined: () => + addCondition({ + type: 'object', + properties: { + $form: { + type: 'object', + not: { + type: 'object', + required: [fieldId] } - }, - required: [fieldId] - } - }, - required: ['$form'] - }), - isInArray: (values: string[]) => ({ - type: 'object', - properties: { - $form: { + } + }, + required: ['$form'] + }), + inArray: (values: string[]) => + addCondition({ + type: 'object', + properties: { + $form: { + type: 'object', + properties: { + [fieldId]: { + enum: values + } + }, + required: [fieldId] + } + }, + required: ['$form'] + }), + not: { + inArray: (values: string[]) => + addCondition({ type: 'object', properties: { - [fieldId]: { - enum: values + $form: { + type: 'object', + properties: { + [fieldId]: { + not: { + enum: values + } + } + }, + required: [fieldId] } }, - required: [fieldId] - } - }, - required: ['$form'] - }), - isNotInArray: (values: string[]) => ({ - type: 'object', - properties: { - $form: { + required: ['$form'] + }), + equalTo: (value: string) => + addCondition({ type: 'object', properties: { - [fieldId]: { - not: { - enum: values - } - } - }, - required: [fieldId] - } - }, - required: ['$form'] - }), - isUndefinedOrInArray: (values: string[]) => ({ - type: 'object', - properties: { - $form: { - type: 'object', - anyOf: [ - { - required: [fieldId], + $form: { + type: 'object', properties: { [fieldId]: { - enum: values + not: { + const: value + } } - } - }, - { not: { required: [fieldId] } } - ] - } - }, - required: ['$form'] - }), - isUndefinedOrNotInArray: (values: string[]) => ({ + }, + required: [fieldId] + } + }, + required: ['$form'] + }) + }, + or: (callback: (field: FieldAPI) => FieldAPI) => { + const nestedConditions = callback(field(fieldId))._apply() + + return addCondition(ensureWrapper(nestedConditions, 'or')) + }, + _apply: () => conditions, + apply: () => { + if (conditions.length === 1) { + return conditions[0] + } + + return ensureWrapper(conditions, 'and') + } + } + + return api +} + +type BooleanConnector = 'and' | 'or' + +/** + * Makes sure JSON Schema conditions are wrapped in an object with a $form property. + */ +const ensureWrapper = ( + conditions: JSONSchema[], + booleanConnector: BooleanConnector +) => { + const conditionsWithConnector = ( + conditions: JSONSchema[], + connector: BooleanConnector + ) => (connector === 'and' ? { allOf: conditions } : { anyOf: conditions }) + + const needsWrapper = conditions.some( + (condition) => + !( + condition.type === 'object' && + condition.properties && + condition.properties.$form + ) + ) + + if (needsWrapper) { + return { type: 'object', properties: { $form: { type: 'object', - anyOf: [ - { - required: [fieldId], - properties: { - [fieldId]: { - not: { - enum: values - } - } - } - }, - { not: { required: [fieldId] } } - ] + ...conditionsWithConnector(conditions, booleanConnector) } - }, - required: ['$form'] - }) + } + } } + + return conditionsWithConnector(conditions, booleanConnector) }