Skip to content

Commit eb26fab

Browse files
feat(next): custom validations with json logic
* chore: first commit (WIP) * feat: add json-logic validation * chore: include logic for form and types * chore: add tests and comments * chore: remove unnecssary options argument * chore: fix lint * chore: revert format on package.json
1 parent 4f21305 commit eb26fab

File tree

13 files changed

+404
-51
lines changed

13 files changed

+404
-51
lines changed

next/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,17 @@
4141
"release:dev": "cd .. && npm run release:v1:dev",
4242
"release:beta": "cd .. && npm run release:v1:beta"
4343
},
44+
"dependencies": {
45+
"json-logic-js": "^2.0.5"
46+
},
4447
"devDependencies": {
4548
"@antfu/eslint-config": "^3.14.0",
4649
"@babel/core": "^7.23.7",
4750
"@babel/preset-env": "^7.23.7",
4851
"@babel/preset-typescript": "^7.26.0",
4952
"@jest/globals": "^29.7.0",
5053
"@jest/reporters": "^29.7.0",
54+
"@types/json-logic-js": "^2.0.8",
5155
"@types/lodash": "^4.17.16",
5256
"@types/validator": "^13.12.2",
5357
"babel-jest": "^29.7.0",

next/pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

next/src/errors/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export type SchemaValidationErrorType =
2929
* Array validation keywords
3030
*/
3131
| 'minItems' | 'maxItems' | 'uniqueItems' | 'contains' | 'maxContains' | 'minContains'
32+
/**
33+
* Custom validation keywords
34+
*/
35+
| 'json-logic'
3236

3337
export type ValidationErrorPath = Array<string | number>
3438

@@ -55,4 +59,10 @@ export interface ValidationError {
5559
* 'required'
5660
*/
5761
validation: SchemaValidationErrorType
62+
/**
63+
* The custom error message to display
64+
* @example
65+
* 'The value is not valid'
66+
*/
67+
customErrorMessage?: string
5868
}

next/src/errors/messages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export function getErrorMessage(
77
schema: NonBooleanJsfSchema,
88
value: SchemaValue,
99
validation: SchemaValidationErrorType,
10+
customErrorMessage?: string,
1011
): string {
1112
switch (validation) {
1213
// Core validation
@@ -76,6 +77,8 @@ export function getErrorMessage(
7677
throw new Error('Array support is not implemented yet')
7778
case 'maxContains':
7879
throw new Error('Array support is not implemented yet')
80+
case 'json-logic':
81+
return customErrorMessage || 'The value is not valid'
7982
}
8083
}
8184

next/src/form.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ function addErrorMessages(rootValue: SchemaValue, rootSchema: JsfSchema, errors:
186186

187187
return {
188188
...error,
189-
message: getErrorMessage(errorSchema, errorValue, error.validation),
189+
message: getErrorMessage(errorSchema, errorValue, error.validation, error.customErrorMessage),
190190
}
191191
})
192192
}

next/src/types.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { RulesLogic } from 'json-logic-js'
12
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
23
import type { FieldType } from './field/type'
3-
44
/**
55
* Defines the type of a `Field` in the form.
66
*/
@@ -29,6 +29,21 @@ export type JsfPresentation = {
2929
[key: string]: unknown
3030
}
3131

32+
export interface JsonLogicBag {
33+
schema: JsonLogicSchema
34+
value: SchemaValue
35+
}
36+
37+
export interface JsonLogicSchema {
38+
validations?: Record<string, {
39+
errorMessage?: string
40+
rule: RulesLogic
41+
}>
42+
computedValues?: Record<string, {
43+
rule: RulesLogic
44+
}>
45+
}
46+
3247
/**
3348
* JSON Schema Form extending JSON Schema with additional JSON Schema Form properties.
3449
*/
@@ -42,17 +57,21 @@ export type JsfSchema = JSONSchema & {
4257
'if'?: JsfSchema
4358
'then'?: JsfSchema
4459
'else'?: JsfSchema
45-
'x-jsf-logic'?: {
46-
validations: Record<string, object>
47-
computedValues: Record<string, object>
48-
}
4960
// Note: if we don't have this property here, when inspecting any recursive
5061
// schema (like an if inside another schema), the required property won't be
5162
// present in the type
5263
'required'?: string[]
64+
// Defines the order of the fields in the form.
5365
'x-jsf-order'?: string[]
66+
// Defines the presentation of the field in the form.
5467
'x-jsf-presentation'?: JsfPresentation
68+
// Defines the error message of the field in the form.
5569
'x-jsf-errorMessage'?: Record<string, string>
70+
'x-jsf-logic'?: JsonLogicSchema
71+
// Extra validations to run. References validations in the `x-jsf-logic` root property.
72+
'x-jsf-logic-validations'?: string[]
73+
// Extra attributes to add to the schema. References computedValues in the `x-jsf-logic` root property.
74+
'x-jsf-logic-computedAttrs'?: Record<keyof JsfSchema, string>
5675
}
5776

5877
/**

next/src/validation/array.ts

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ValidationError, ValidationErrorPath } from '../errors'
2-
import type { JsfSchema, NonBooleanJsfSchema, SchemaValue } from '../types'
2+
import type { JsfSchema, JsonLogicBag, NonBooleanJsfSchema, SchemaValue } from '../types'
33
import { validateSchema, type ValidationOptions } from './schema'
44
import { deepEqual } from './util'
55

@@ -8,6 +8,7 @@ import { deepEqual } from './util'
88
* @param value - The value to validate
99
* @param schema - The schema to validate against
1010
* @param options - The validation options
11+
* @param jsonLogicBag - The JSON logic bag
1112
* @param path - The path to the current field being validated
1213
* @returns An array of validation errors
1314
* @description
@@ -18,6 +19,7 @@ export function validateArray(
1819
value: SchemaValue,
1920
schema: JsfSchema,
2021
options: ValidationOptions,
22+
jsonLogicBag: JsonLogicBag | undefined,
2123
path: ValidationErrorPath,
2224
): ValidationError[] {
2325
if (!Array.isArray(value)) {
@@ -27,9 +29,9 @@ export function validateArray(
2729
return [
2830
...validateLength(schema, value, path),
2931
...validateUniqueItems(schema, value, path),
30-
...validateContains(value, schema, options, path),
31-
...validatePrefixItems(schema, value, options, path),
32-
...validateItems(schema, value, options, path),
32+
...validateContains(value, schema, options, jsonLogicBag, path),
33+
...validatePrefixItems(schema, value, options, jsonLogicBag, path),
34+
...validateItems(schema, value, options, jsonLogicBag, path),
3335
]
3436
}
3537

@@ -44,7 +46,11 @@ export function validateArray(
4446
* If the `maxItems` keyword is defined, the array must contain at most `maxItems` items.
4547
* If the `minItems` keyword is defined, the array must contain at least `minItems` items.
4648
*/
47-
function validateLength(schema: NonBooleanJsfSchema, value: SchemaValue[], path: ValidationErrorPath): ValidationError[] {
49+
function validateLength(
50+
schema: NonBooleanJsfSchema,
51+
value: SchemaValue[],
52+
path: ValidationErrorPath,
53+
): ValidationError[] {
4854
const errors: ValidationError[] = []
4955

5056
const itemsLength = value.length
@@ -65,14 +71,21 @@ function validateLength(schema: NonBooleanJsfSchema, value: SchemaValue[], path:
6571
* @param schema - The schema to validate against
6672
* @param values - The array value to validate
6773
* @param options - The validation options
74+
* @param jsonLogicBag - The JSON logic bag
6875
* @param path - The path to the current field being validated
6976
* @returns An array of validation errors
7077
* @description
7178
* Validates the items constraint of an array.
7279
* If the `items` keyword is defined, each item in the array must match the schema of the `items` keyword.
7380
* When the `prefixItems` keyword is defined, the items constraint is validated only for the items after the prefix items.
7481
*/
75-
function validateItems(schema: NonBooleanJsfSchema, values: SchemaValue[], options: ValidationOptions, path: ValidationErrorPath): ValidationError[] {
82+
function validateItems(
83+
schema: NonBooleanJsfSchema,
84+
values: SchemaValue[],
85+
options: ValidationOptions,
86+
jsonLogicBag: JsonLogicBag | undefined,
87+
path: ValidationErrorPath,
88+
): ValidationError[] {
7689
if (schema.items === undefined) {
7790
return []
7891
}
@@ -81,7 +94,15 @@ function validateItems(schema: NonBooleanJsfSchema, values: SchemaValue[], optio
8194
const startIndex = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
8295

8396
for (const [i, item] of values.slice(startIndex).entries()) {
84-
errors.push(...validateSchema(item, schema.items, options, [...path, 'items', i + startIndex]))
97+
errors.push(
98+
...validateSchema(
99+
item,
100+
schema.items,
101+
options,
102+
[...path, 'items', i + startIndex],
103+
jsonLogicBag,
104+
),
105+
)
85106
}
86107

87108
return errors
@@ -92,21 +113,36 @@ function validateItems(schema: NonBooleanJsfSchema, values: SchemaValue[], optio
92113
* @param schema - The schema to validate against
93114
* @param values - The array value to validate
94115
* @param options - The validation options
116+
* @param jsonLogicBag - The JSON logic bag
95117
* @param path - The path to the current field being validated
96118
* @returns An array of validation errors
97119
* @description
98120
* Validates the prefixItems constraint of an array.
99121
* If the `prefixItems` keyword is defined, each item in the array must match the schema of the corresponding prefix item.
100122
*/
101-
function validatePrefixItems(schema: NonBooleanJsfSchema, values: SchemaValue[], options: ValidationOptions, path: ValidationErrorPath): ValidationError[] {
123+
function validatePrefixItems(
124+
schema: NonBooleanJsfSchema,
125+
values: SchemaValue[],
126+
options: ValidationOptions,
127+
jsonLogicBag: JsonLogicBag | undefined,
128+
path: ValidationErrorPath,
129+
): ValidationError[] {
102130
if (!Array.isArray(schema.prefixItems)) {
103131
return []
104132
}
105133

106134
const errors: ValidationError[] = []
107135
for (const [i, item] of values.entries()) {
108136
if (i < schema.prefixItems.length) {
109-
errors.push(...validateSchema(item, schema.prefixItems[i] as JsfSchema, options, [...path, 'prefixItems', i]))
137+
errors.push(
138+
...validateSchema(
139+
item,
140+
schema.prefixItems[i] as JsfSchema,
141+
options,
142+
[...path, 'prefixItems', i],
143+
jsonLogicBag,
144+
),
145+
)
110146
}
111147
}
112148

@@ -118,6 +154,7 @@ function validatePrefixItems(schema: NonBooleanJsfSchema, values: SchemaValue[],
118154
* @param value - The array value to validate
119155
* @param schema - The schema to validate against
120156
* @param options - The validation options
157+
* @param jsonLogicBag - The JSON logic bag
121158
* @param path - The path to the current field being validated
122159
* @returns An array of validation errors
123160
* @description
@@ -128,6 +165,7 @@ function validateContains(
128165
value: SchemaValue[],
129166
schema: NonBooleanJsfSchema,
130167
options: ValidationOptions,
168+
jsonLogicBag: JsonLogicBag | undefined,
131169
path: ValidationErrorPath,
132170
): ValidationError[] {
133171
if (!('contains' in schema)) {
@@ -137,8 +175,15 @@ function validateContains(
137175
const errors: ValidationError[] = []
138176

139177
// How many items in the array are valid against the contains schema?
140-
const contains = value.filter(item =>
141-
validateSchema(item, schema.contains as JsfSchema, options, [...path, 'contains']).length === 0,
178+
const contains = value.filter(
179+
item =>
180+
validateSchema(
181+
item,
182+
schema.contains as JsfSchema,
183+
options,
184+
[...path, 'contains'],
185+
jsonLogicBag,
186+
).length === 0,
142187
).length
143188

144189
if (schema.minContains === undefined && schema.maxContains === undefined) {
@@ -168,7 +213,11 @@ function validateContains(
168213
* @description
169214
* Validates the uniqueItems constraint of an array when the `uniqueItems` keyword is defined as `true`.
170215
*/
171-
function validateUniqueItems(schema: NonBooleanJsfSchema, values: SchemaValue[], path: ValidationErrorPath): ValidationError[] {
216+
function validateUniqueItems(
217+
schema: NonBooleanJsfSchema,
218+
values: SchemaValue[],
219+
path: ValidationErrorPath,
220+
): ValidationError[] {
172221
if (schema.uniqueItems !== true) {
173222
return []
174223
}

0 commit comments

Comments
 (0)