@nolwenture/formik-jsonschema-form 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,6 +69,125 @@ const simpleExampleSchema = {
69
69
  simpleArray: { type: "array", title: "Simple array" },
70
70
  },
71
71
  };
72
+ const dependenciesExampleSchema = {
73
+ properties: {
74
+ dependencyField: { type: "string" },
75
+ dependencySelect: {
76
+ type: "string",
77
+ oneOf: [
78
+ { const: "one", title: "One" },
79
+ { const: "two", title: "Two" },
80
+ ],
81
+ },
82
+ },
83
+ dependencies: {
84
+ dependencyField: {
85
+ properties: {
86
+ dependantField: { type: "string" },
87
+ },
88
+ required: ["dependantField"],
89
+ },
90
+ dependencySelect: {
91
+ oneOf: [
92
+ {
93
+ properties: {
94
+ dependencySelect: {
95
+ enum: ["one"],
96
+ },
97
+ },
98
+ },
99
+ {
100
+ properties: {
101
+ dependencySelect: {
102
+ enum: ["two"],
103
+ },
104
+ dependantOnTwo: {
105
+ type: "string",
106
+ },
107
+ },
108
+ required: ["dependantOnTwo"],
109
+ },
110
+ ],
111
+ },
112
+ },
113
+ };
114
+ const ifThenElseExampleSchema = {
115
+ type: "object",
116
+ properties: {
117
+ allOfBool1: { type: "boolean" },
118
+ allOfBool2: { type: "boolean" },
119
+ },
120
+ allOf: [
121
+ {
122
+ if: {
123
+ properties: {
124
+ allOfBool1: { const: true },
125
+ },
126
+ },
127
+ then: {
128
+ properties: {
129
+ firstTrueRequiredField: { type: "string" },
130
+ },
131
+ required: ["firstTrueRequiredField"],
132
+ },
133
+ else: {
134
+ properties: {
135
+ firstFalseRequiredField: { type: "string" },
136
+ },
137
+ required: ["firstFalseRequiredField"],
138
+ },
139
+ },
140
+ {
141
+ if: {
142
+ properties: {
143
+ allOfBool1: { const: false },
144
+ allOfBool2: { const: false },
145
+ },
146
+ },
147
+ then: {
148
+ properties: {
149
+ bothFalseRequiredField: { type: "string" },
150
+ },
151
+ required: ["bothFalseRequiredField"],
152
+ },
153
+ else: {
154
+ properties: {
155
+ oneOrMoreTrueRequiredField: { type: "string" },
156
+ },
157
+ required: ["oneOrMoreTrueRequiredField"],
158
+ },
159
+ },
160
+ ],
161
+ };
162
+ const nestedRequiredConditionalExampleSchema = {
163
+ type: "object",
164
+ properties: {
165
+ textField: { type: "string" },
166
+ checkbox: { type: "boolean" },
167
+ },
168
+ allOf: [
169
+ {
170
+ if: { properties: { checkbox: { const: true } } },
171
+ then: {
172
+ properties: {
173
+ myObject: {
174
+ type: "object",
175
+ properties: {
176
+ foo: { type: "string" },
177
+ nestedObject: {
178
+ type: "object",
179
+ properties: { nestedFoo: { type: "string" } },
180
+ required: ["nestedFoo"],
181
+ },
182
+ },
183
+ required: ["foo"],
184
+ },
185
+ },
186
+ required: ["myObject"],
187
+ },
188
+ },
189
+ ],
190
+ };
72
191
  describe("Create Validation Schema", () => {
73
192
  it("creates yup validationSchema based on given JSON schema", () => __awaiter(void 0, void 0, void 0, function* () {
74
193
  const result = createValidationSchema(exampleSchema);
@@ -133,4 +252,130 @@ describe("Create Validation Schema", () => {
133
252
  simpleArray: "Hello :)",
134
253
  }, { strict: true })).rejects.toThrow('simpleArray must be a `array` type, but the final value was: `"Hello :)"`.');
135
254
  }));
255
+ describe("Conditionals", () => {
256
+ it("generates validation for dependencies without oneOf", () => {
257
+ const result = createValidationSchema(dependenciesExampleSchema);
258
+ const invalid = result.isValidSync({
259
+ dependencyField: "Hello",
260
+ });
261
+ const valid = result.isValidSync({
262
+ dependencyField: "Hello",
263
+ dependantField: "Hi",
264
+ });
265
+ expect(invalid).toEqual(false);
266
+ expect(valid).toEqual(true);
267
+ });
268
+ it("generates validation for dependencies with oneOf", () => {
269
+ const result = createValidationSchema(dependenciesExampleSchema);
270
+ const noDependantFields = result.isValidSync({
271
+ dependencySelect: "one",
272
+ });
273
+ const invalidWithDependantFields = result.isValidSync({
274
+ dependencySelect: "two",
275
+ });
276
+ const validWithDependantFields = result.isValidSync({
277
+ dependencySelect: "two",
278
+ dependantOnTwo: "value",
279
+ });
280
+ expect(noDependantFields).toEqual(true);
281
+ expect(invalidWithDependantFields).toEqual(false);
282
+ expect(validWithDependantFields).toEqual(true);
283
+ });
284
+ it("generates validation for if-then-else", () => {
285
+ const result = createValidationSchema(ifThenElseExampleSchema);
286
+ const bothFalseInvalid1 = result.isValidSync({
287
+ allOfBool1: false,
288
+ allOfBool2: false,
289
+ });
290
+ const bothFalseInvalid2 = result.isValidSync({
291
+ allOfBool1: false,
292
+ allOfBool2: false,
293
+ firstFalseRequiredField: ".",
294
+ });
295
+ const bothFalseInvalid3 = result.isValidSync({
296
+ allOfBool1: false,
297
+ allOfBool2: false,
298
+ bothFalseRequiredField: ".",
299
+ });
300
+ const bothFalseValid = result.isValidSync({
301
+ allOfBool1: false,
302
+ allOfBool2: false,
303
+ firstFalseRequiredField: ".",
304
+ bothFalseRequiredField: ".",
305
+ });
306
+ const firstFalseInvalid1 = result.isValidSync({
307
+ allOfBool1: false,
308
+ allOfBool2: true,
309
+ });
310
+ const firstFalseInvalid2 = result.isValidSync({
311
+ allOfBool1: false,
312
+ allOfBool2: true,
313
+ firstFalseRequiredField: ".",
314
+ });
315
+ const firstFalseInvalid3 = result.isValidSync({
316
+ allOfBool1: false,
317
+ allOfBool2: true,
318
+ oneOrMoreTrueRequiredField: ".",
319
+ });
320
+ const firstFalseValid = result.isValidSync({
321
+ allOfBool1: false,
322
+ allOfBool2: true,
323
+ firstFalseRequiredField: ".",
324
+ oneOrMoreTrueRequiredField: ".",
325
+ });
326
+ const firstTrueInvalid1 = result.isValidSync({
327
+ allOfBool1: true,
328
+ allOfBool2: false,
329
+ });
330
+ const firstTrueInvalid2 = result.isValidSync({
331
+ allOfBool1: true,
332
+ allOfBool2: false,
333
+ firstTrueRequiredField: ".",
334
+ });
335
+ const firstTrueInvalid3 = result.isValidSync({
336
+ allOfBool1: true,
337
+ allOfBool2: false,
338
+ oneOrMoreTrueRequiredField: ".",
339
+ });
340
+ const firstTrueValid = result.isValidSync({
341
+ allOfBool1: true,
342
+ allOfBool2: false,
343
+ firstTrueRequiredField: ".",
344
+ oneOrMoreTrueRequiredField: ".",
345
+ });
346
+ expect(bothFalseInvalid1).toEqual(false);
347
+ expect(bothFalseInvalid2).toEqual(false);
348
+ expect(bothFalseInvalid3).toEqual(false);
349
+ expect(bothFalseValid).toEqual(true);
350
+ expect(firstFalseInvalid1).toEqual(false);
351
+ expect(firstFalseInvalid2).toEqual(false);
352
+ expect(firstFalseInvalid3).toEqual(false);
353
+ expect(firstFalseValid).toEqual(true);
354
+ expect(firstTrueInvalid1).toEqual(false);
355
+ expect(firstTrueInvalid2).toEqual(false);
356
+ expect(firstTrueInvalid3).toEqual(false);
357
+ expect(firstTrueValid).toEqual(true);
358
+ });
359
+ it("generates validation for conditional objects with nested validations", () => {
360
+ const result = createValidationSchema(nestedRequiredConditionalExampleSchema);
361
+ const isValidWhenConditionNotNet = result.isValidSync({
362
+ checkbox: false,
363
+ });
364
+ const isNotValidWhenConditionMetAndMissingRequiredFields = result.isValidSync({
365
+ checkbox: true,
366
+ });
367
+ const isValidWhenConditionMetAndRequiredFieldsAreFilled = result.isValidSync({
368
+ checkbox: true,
369
+ myObject: {
370
+ foo: ".",
371
+ nestedObject: {
372
+ nestedFoo: ".",
373
+ },
374
+ },
375
+ });
376
+ expect(isValidWhenConditionNotNet).toEqual(true);
377
+ expect(isNotValidWhenConditionMetAndMissingRequiredFields).toEqual(false);
378
+ expect(isValidWhenConditionMetAndRequiredFieldsAreFilled).toEqual(true);
379
+ });
380
+ });
136
381
  });
@@ -1,3 +1,8 @@
1
- import { JSONSchema7 } from "json-schema";
2
1
  import * as Yup from "yup";
3
- export declare function createValidationSchema(schema: JSONSchema7, definitions?: JSONSchema7): Yup.ObjectSchema<{}, Yup.AnyObject, {}, "">;
2
+ import { Schema } from "../types";
3
+ type Condition = {
4
+ dependentKeys: string[];
5
+ conditionFn: (...value: any[]) => boolean;
6
+ };
7
+ export declare function createValidationSchema(schema: Schema, definitions?: Schema, condition?: Condition): Yup.ObjectSchema<{}, Yup.AnyObject, {}, "">;
8
+ export {};
@@ -1,54 +1,164 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { solveReferences } from "./solveReferences";
2
13
  import * as Yup from "yup";
3
- export function createValidationSchema(schema, definitions = {}) {
14
+ import { resolveOneOfValue } from "./resolveSchema";
15
+ function getConditionalValidations(schema) {
16
+ const { dependencies, allOf } = schema;
17
+ let conditionalValidations = [];
18
+ if (dependencies) {
19
+ conditionalValidations = Object.entries(dependencies).reduce((validations, [key, dependency]) => {
20
+ if (typeof dependency === "boolean" || Array.isArray(dependency)) {
21
+ return validations;
22
+ }
23
+ const oneOf = dependency.oneOf;
24
+ if (Array.isArray(oneOf)) {
25
+ oneOf.forEach((oneOfSchema) => {
26
+ var _a, _b;
27
+ if (typeof oneOfSchema === "boolean") {
28
+ return;
29
+ }
30
+ const _c = (_a = oneOfSchema.properties) !== null && _a !== void 0 ? _a : {}, _d = key, propertyToCheck = _c[_d], propertiesSansKey = __rest(_c, [typeof _d === "symbol" ? _d : _d + ""]);
31
+ // Push validation only if there are additions in schema
32
+ if (((_b = Object.keys(propertiesSansKey)) === null || _b === void 0 ? void 0 : _b.length) ||
33
+ Object.keys(oneOfSchema).length > 1) {
34
+ validations.push({
35
+ schema: Object.assign(Object.assign({}, oneOfSchema), { properties: propertiesSansKey }),
36
+ condition: {
37
+ dependentKeys: [key],
38
+ conditionFn: (value) => value === resolveOneOfValue(propertyToCheck),
39
+ },
40
+ });
41
+ }
42
+ });
43
+ return validations;
44
+ }
45
+ // Depends on presence of `key` property value
46
+ validations.push({
47
+ schema: dependency,
48
+ condition: {
49
+ dependentKeys: [key],
50
+ conditionFn: (value) => value !== undefined && value !== "",
51
+ },
52
+ });
53
+ return validations;
54
+ }, conditionalValidations);
55
+ }
56
+ if (allOf) {
57
+ conditionalValidations = allOf.reduce((validations, condition) => {
58
+ if (typeof condition === "boolean") {
59
+ return validations;
60
+ }
61
+ const { if: expression, then, else: otherwise } = condition, schemaWithoutCondition = __rest(condition, ["if", "then", "else"]);
62
+ if (expression &&
63
+ typeof expression !== "boolean" &&
64
+ expression.properties) {
65
+ const keyValueMap = Object.entries(expression.properties).reduce((result, [key, value]) => {
66
+ if (typeof value !== "boolean") {
67
+ return Object.assign(Object.assign({}, result), { [key]: value.const });
68
+ }
69
+ return result;
70
+ }, {});
71
+ const dependentKeys = Object.keys(keyValueMap);
72
+ if (then && typeof then !== "boolean") {
73
+ validations.push({
74
+ schema: then,
75
+ condition: {
76
+ dependentKeys,
77
+ conditionFn: (...args) => args.every((value, index) => keyValueMap[dependentKeys[index]] === value),
78
+ },
79
+ });
80
+ }
81
+ if (otherwise && typeof otherwise !== "boolean") {
82
+ validations.push({
83
+ schema: otherwise,
84
+ condition: {
85
+ dependentKeys,
86
+ conditionFn: (...args) => args.some((value, index) => keyValueMap[dependentKeys[index]] !== value),
87
+ },
88
+ });
89
+ }
90
+ }
91
+ else {
92
+ validations.push({ schema: schemaWithoutCondition });
93
+ }
94
+ return validations;
95
+ }, conditionalValidations);
96
+ }
97
+ return conditionalValidations;
98
+ }
99
+ function getYupType(type) {
100
+ switch (type) {
101
+ case "object":
102
+ return Yup.object();
103
+ case "array":
104
+ return Yup.array();
105
+ case "boolean":
106
+ return Yup.boolean();
107
+ case "integer":
108
+ case "number":
109
+ return Yup.number();
110
+ case "null":
111
+ return Yup.string();
112
+ case "string":
113
+ return Yup.string();
114
+ default:
115
+ throw new Error("Unsupported schema type");
116
+ }
117
+ }
118
+ export function createValidationSchema(schema, definitions = {}, condition) {
4
119
  var _a;
5
- if (!schema || !schema.properties) {
120
+ if (!(schema === null || schema === void 0 ? void 0 : schema.properties)) {
6
121
  return Yup.object().shape({});
7
122
  }
8
123
  const schemaDefinitions = Object.assign(Object.assign({}, definitions), ((_a = schema.definitions) !== null && _a !== void 0 ? _a : {}));
9
124
  const requiredFields = schema.required;
10
125
  function getValue(schema, key) {
126
+ let type = schema.type;
127
+ let yupSchema;
11
128
  if (schema.properties) {
12
- return createValidationSchema(schema, schemaDefinitions);
129
+ yupSchema = createValidationSchema(schema, schemaDefinitions);
130
+ type = type !== null && type !== void 0 ? type : "object";
13
131
  }
14
- const type = schema.type;
15
- let yupType;
16
- switch (type) {
17
- case "array":
18
- yupType = Yup.array();
19
- break;
20
- case "boolean":
21
- yupType = Yup.boolean();
22
- break;
23
- case "integer":
24
- case "number":
25
- yupType = Yup.number();
26
- break;
27
- case "null":
28
- yupType = Yup.string().nullable();
29
- break;
30
- case "string":
31
- yupType = Yup.string();
32
- break;
33
- default:
34
- throw new Error("Unsupported schema type");
132
+ else {
133
+ yupSchema = getYupType(type);
35
134
  }
36
- if (requiredFields !== undefined && requiredFields.includes(key)) {
135
+ if (type === "null")
136
+ yupSchema.nullable();
137
+ if (requiredFields === null || requiredFields === void 0 ? void 0 : requiredFields.includes(key)) {
37
138
  if (type === "array") {
38
- yupType = yupType
139
+ yupSchema = yupSchema
39
140
  .min(1, "Required")
40
141
  .required("Required");
41
142
  }
42
143
  else {
43
- yupType = yupType["required"]("Required");
144
+ yupSchema = yupSchema["required"]("Required");
44
145
  }
45
146
  }
46
- return yupType;
147
+ return condition
148
+ ? getYupType(type).when(condition.dependentKeys, {
149
+ is: condition.conditionFn,
150
+ then: () => yupSchema,
151
+ })
152
+ : yupSchema;
47
153
  }
48
- return Object.keys(schema.properties).reduce((validationSchema, key) => {
154
+ const baseValidationSchema = Object.keys(schema.properties).reduce((validationSchema, key) => {
49
155
  var _a;
50
156
  const value = (_a = schema.properties) === null || _a === void 0 ? void 0 : _a[key];
51
157
  const solvedValue = solveReferences(value, schemaDefinitions, {}, {});
52
158
  return validationSchema.concat(Yup.object().shape({ [key]: getValue(solvedValue.schema, key) }));
53
159
  }, Yup.object().shape({}));
160
+ return getConditionalValidations(schema).reduce((validationSchema, conditionalValidation) => {
161
+ const solvedSchemas = solveReferences(conditionalValidation.schema, schemaDefinitions, {}, {});
162
+ return validationSchema.concat(createValidationSchema(solvedSchemas.schema, schemaDefinitions, conditionalValidation.condition));
163
+ }, baseValidationSchema);
54
164
  }
@@ -1,2 +1,4 @@
1
+ import { JSONSchema7Definition } from "json-schema";
1
2
  import type { Schema } from "../types";
3
+ export declare function resolveOneOfValue(value: JSONSchema7Definition): import("json-schema").JSONSchema7Type | undefined;
2
4
  export declare const resolveSchema: ({ allOf, dependencies, ...schema }: Schema, value: Record<string, any>) => [Schema, string[]];
@@ -19,7 +19,7 @@ const resolveEquality = (value, expression) => {
19
19
  };
20
20
  return deepOverlap(expression, newObject);
21
21
  };
22
- function resolveOneOfValue(value) {
22
+ export function resolveOneOfValue(value) {
23
23
  var _a;
24
24
  if (typeof value !== "boolean" && ((_a = value.enum) === null || _a === void 0 ? void 0 : _a[0]) !== undefined) {
25
25
  return value.enum[0];
@@ -31,14 +31,18 @@ function resolveOneOfValue(value) {
31
31
  return undefined;
32
32
  }
33
33
  const resolveSelectedOneOfValue = (key, value, oneOf) => {
34
- const update = oneOf.findIndex((one) => resolveOneOfValue(one.properties[key]) === value[key]);
35
- const manipulatedUpdateValue = Object.keys(oneOf[update].properties)
34
+ var _a, _b;
35
+ const update = ((_a = oneOf.find((one) => typeof one !== "boolean" && (one === null || one === void 0 ? void 0 : one.properties)
36
+ ? resolveOneOfValue(one.properties[key]) === value[key]
37
+ : false)) !== null && _a !== void 0 ? _a : {});
38
+ const manipulatedUpdateValue = Object.keys((_b = update.properties) !== null && _b !== void 0 ? _b : {})
36
39
  .filter((objKey) => objKey !== key)
37
40
  .reduce((newObj, innerKey) => {
38
- newObj[innerKey] = oneOf[update].properties[innerKey];
41
+ var _a;
42
+ newObj[innerKey] = (_a = update.properties) === null || _a === void 0 ? void 0 : _a[innerKey];
39
43
  return newObj;
40
44
  }, {});
41
- return { properties: manipulatedUpdateValue };
45
+ return Object.assign(Object.assign({}, update), { properties: manipulatedUpdateValue });
42
46
  };
43
47
  export const resolveSchema = (_a, value) => {
44
48
  var _b, _c;
@@ -48,7 +52,10 @@ export const resolveSchema = (_a, value) => {
48
52
  let valuesToRemoveAllOf = [];
49
53
  if (dependencies) {
50
54
  schemaCopy = Object.entries(dependencies).reduce((schema, [key, dependency]) => {
51
- if (value[key] !== undefined && value[key] !== "") {
55
+ if (value[key] !== undefined &&
56
+ value[key] !== "" &&
57
+ typeof dependency !== "boolean" &&
58
+ !Array.isArray(dependency)) {
52
59
  const oneOf = dependency.oneOf;
53
60
  if (Array.isArray(oneOf) && oneOf !== undefined) {
54
61
  const selectedOneOf = resolveSelectedOneOfValue(key, value, oneOf);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nolwenture/formik-jsonschema-form",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Formik form generation from json schema",
5
5
  "author": "Antti Vikman <antti.vikman@nolwenture.com>",
6
6
  "contributors": [