Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@
"test": "jest",
"test:watch": "jest --watchAll",
"test:file": "jest --runTestsByPath --watch",
"test:v0-update-baseline": "jest --roots '<rootDir>/../src/tests' --json --outputFile=test/v0-baseline-test-results.json",
"test:v0-update-baseline": "cd v0 && jest --json --outputFile=../test/v0-baseline-test-results.json;",
"test:v0-compare-results": "node test/v0_compare_test_results.js",
"test:v0": "jest --roots '<rootDir>/../src/tests' --json --outputFile=test/v0-test-results.json; pnpm run test:v0-compare-results",
"test:v0": "cd v0 && jest --json --outputFile=../test/v0-test-results.json; cd ..; pnpm run test:v0-compare-results",
"lint": "eslint --max-warnings 0 .",
"typecheck": "tsc --noEmit",
"check": "pnpm run lint && pnpm run typecheck",
"check:pr-next-version": "node ../scripts/pr_next_dev_version",
"check:pr-next-version": "node ./scripts/pr_next_dev_version",
"release:dev": "node ./scripts/release dev",
"release:beta": "node ./scripts/release beta",
"release": "node ./scripts/release official"
Expand Down
9 changes: 6 additions & 3 deletions src/field/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,23 +378,26 @@ export function buildFieldSchema({
if (schema === false) {
// If the schema is false (hidden field), we use the original schema to get the input type
const inputType = getInputType(type, name, originalSchema, strictInputType)
const inputHasInnerFields = ['fieldset', 'group-array'].includes(inputType)

const hiddenField: Field = {
type: inputType,
name,
inputType,
jsonType: 'boolean',
jsonType: type || originalSchema.type,
required,
isVisible: false,
...(inputHasInnerFields && { fields: [] }),
}

// Preserve default from originalSchema for hidden fields (important for GROUP_ARRAY fields)
if (originalSchema.default !== undefined) {
hiddenField.default = originalSchema.default
}

// We still build nested fields if the parent schema is deemed false
// These allow the fields to be initialized by a form, and their default values to be set for when they're displayed
// We pass originalSchema instead of schema since schema is false at this point
addFields(hiddenField, originalSchema, originalSchema)

return hiddenField
}

Expand Down
13 changes: 7 additions & 6 deletions src/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,13 @@ function applySchemaRules(
}

// If the schema has an allOf property, evaluate each rule and add it to the conditional rules array
(schema.allOf ?? [])
.filter((rule: JsfSchema) => typeof rule.if !== 'undefined')
.forEach((rule) => {
const result = evaluateConditional(values, schema, rule as NonBooleanJsfSchema, options, jsonLogicContext)
conditionalRules.push(result)
})
const allOf = schema.allOf ?? []
const jsonLogicAllOf = schema['x-jsf-logic']?.allOf ?? [];

[...allOf, ...jsonLogicAllOf].filter((rule: JsfSchema) => typeof rule.if !== 'undefined').forEach((rule) => {
const result = evaluateConditional(values, schema, rule, options, jsonLogicContext)
conditionalRules.push(result)
})

// Process the conditional rules
for (const { rule, matches } of conditionalRules) {
Expand Down
9 changes: 8 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ export interface JsonLogicRules {
rule: RulesLogic
}>
}
export interface JsonLogicRootSchema extends Pick<NonBooleanJsfSchema, 'if' | 'then' | 'else' | 'allOf' | 'anyOf' | 'oneOf' | 'not'> {}
export type JsonLogicRootSchema = Pick<NonBooleanJsfSchema, 'if' | 'then' | 'else' | 'anyOf' | 'oneOf' | 'not'> & {
allOf?: (JsfSchema & { if?: JsonLogicIfNodeSchema })[]
}

export type JsonLogicIfNodeSchema = JsfSchema & {
validations?: Record<string, JsfSchema>
computedValues?: Record<string, JsfSchema>
}

export interface JsonLogicSchema extends JsonLogicRules, JsonLogicRootSchema {}

Expand Down
31 changes: 10 additions & 21 deletions src/validation/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,29 +164,18 @@ export function validateOneOf(
}
}

if (validCount === 0) {
return [
{
path,
validation: 'oneOf',
schema,
value,
},
]
}

if (validCount > 1) {
return [
{
path,
validation: 'oneOf',
schema,
value,
},
]
if (validCount === 1) {
return []
}

return []
return [
{
path,
validation: 'oneOf',
schema,
value,
},
]
}

/**
Expand Down
56 changes: 53 additions & 3 deletions src/validation/conditions.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,71 @@
import type { ValidationError, ValidationErrorPath } from '../errors'
import type { LegacyOptions } from '../form'
import type { JsfSchema, JsonLogicContext, NonBooleanJsfSchema, SchemaValue } from '../types'
import type { JsfSchema, JsonLogicContext, JsonLogicIfNodeSchema, NonBooleanJsfSchema, SchemaValue } from '../types'
import { computePropertyValues } from './json-logic'
import { validateSchema } from './schema'

export function evaluateIfCondition(
value: SchemaValue,
ifNode: JsfSchema,
ifNode: JsfSchema | JsonLogicIfNodeSchema,
options: LegacyOptions,
jsonLogicContext: JsonLogicContext | undefined,
path: ValidationErrorPath = [],
): boolean {
// If a boolean value is used as a condition, we need to ignore the allowForbiddenValues option.
// Otherwise, we can't evaluate the condition correctly
const isTheConditionalABoolean = typeof ifNode === 'boolean'

const conditionIsTrue = validateSchema(value, ifNode, { ...options, ...(isTheConditionalABoolean ? { allowForbiddenValues: false } : {}) }, path, jsonLogicContext).length === 0

return conditionIsTrue
const matchedValidations = !isTheConditionalABoolean && 'validations' in ifNode ? validateJsonLogicValidations(value, ifNode.validations, jsonLogicContext) : true

const matchedComputedValues = !isTheConditionalABoolean && 'computedValues' in ifNode ? validateJsonLogicComputedValues(value, ifNode.computedValues, jsonLogicContext) : true

return conditionIsTrue && matchedValidations && matchedComputedValues
}

/**
* Checks if all the rules under the `validations` property of an `if` node are valid.
* These `validations` are defined in the `x-jsf-logic`.allOf.if.validations property of the schema.
*/
function validateJsonLogicValidations(value: SchemaValue, validations: JsfSchema | undefined, jsonLogicContext: JsonLogicContext | undefined): boolean {
if (!jsonLogicContext) {
throw new Error('`if` node with `validations` property but no `jsonLogicContext` present')
}

const allValidationsMatch = Object.entries(validations ?? {}).every(([name, property]) => {
const validationRule = jsonLogicContext?.schema?.validations?.[name]?.rule
if (!validationRule) {
throw new Error(`\`if\` node with \`validations\` property but no validation rule present for ${name}`)
}

const currentValue = computePropertyValues(name, validationRule, value)
return Object.hasOwn(property, 'const') && currentValue === property.const
})

return allValidationsMatch
}

/**
* Checks if all the rules under the `computedValues` property of an `if` node are valid.
* These `computedValues` are defined in the `x-jsf-logic`.allOf.if.computedValues property of the schema.
*/
function validateJsonLogicComputedValues(value: SchemaValue, computedValues: JsfSchema | undefined, jsonLogicContext: JsonLogicContext | undefined): boolean {
if (!jsonLogicContext) {
throw new Error('`if` node with `computedValues` property but no `jsonLogicContext` present')
}

const allComputedValuesMatch = Object.entries(computedValues ?? {}).every(([name, property]) => {
const computedValueRule = jsonLogicContext?.schema?.computedValues?.[name]?.rule
if (!computedValueRule) {
throw new Error(`\`if\` node with \`computedValues\` property but no computed value rule present for ${name}`)
}

const currentValue = computePropertyValues(name, computedValueRule, value)
return Object.hasOwn(property, 'const') && currentValue === property.const
})

return allComputedValuesMatch
}

export function validateCondition(
Expand Down
11 changes: 6 additions & 5 deletions src/validation/json-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,10 @@ function cycleThroughPropertiesAndApplyValues(schemaCopy: JsfSchema, computedVal
cycleThroughPropertiesAndApplyValues(propertySchema.if, computedValues)
}

/* If the schema has an allOf or anyOf property, we need to cycle through each property inside it and
* apply the computed values
*/

/**
* If the schema has an allOf, anyOf or oneOf property, we need to cycle through each property inside it and
* apply the computed values
*/
if (propertySchema.allOf && propertySchema.allOf.length > 0) {
for (const schema of propertySchema.allOf) {
cycleThroughPropertiesAndApplyValues(schema, computedValues)
Expand Down Expand Up @@ -233,7 +233,8 @@ function cycleThroughAttrsAndApplyValues(propertySchema: JsfSchema, computedValu
// If it's a template, we need to interpolate it, replacing the handlebars with the computed value
return message.replace(/\{\{(.*?)\}\}/g, (_, computation) => {
const computationName = computation.trim()
return computedValues[computationName] || `{{${computationName}}}`
// 0 is a valid computation output
return computedValues[computationName] ?? `{{${computationName}}}`
})
}

Expand Down
23 changes: 21 additions & 2 deletions test/fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,27 @@ describe('fields', () => {
{
inputType: 'fieldset',
type: 'fieldset',
jsonType: 'boolean',
fields: [],
jsonType: 'object',
fields: [
{
inputType: 'number',
isVisible: true,
jsonType: 'number',
label: 'Age',
name: 'age',
required: false,
type: 'number',
},
{
inputType: 'number',
isVisible: true,
jsonType: 'number',
label: 'Amount',
name: 'amount',
required: false,
type: 'number',
},
],
name: 'root',
required: true,
isVisible: false,
Expand Down
Loading