Skip to content

Commit

Permalink
Merge pull request #8379 from opencrvs/feat/chainable-conditionals
Browse files Browse the repository at this point in the history
Feat/chainable conditionals
  • Loading branch information
makelicious authored Jan 23, 2025
2 parents bb07654 + 3db8629 commit 247da48
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 96 deletions.
2 changes: 1 addition & 1 deletion packages/toolkit/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
278 changes: 183 additions & 95 deletions packages/toolkit/src/conditionals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
}

0 comments on commit 247da48

Please sign in to comment.