@react-typed-forms/schemas 14.0.3 → 14.0.4

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.
@@ -212,7 +212,7 @@ export interface SchemaDataNode {
212
212
  id: string;
213
213
  schema: SchemaNode;
214
214
  elementIndex?: number;
215
- control?: Control<unknown>;
215
+ control: Control<any>;
216
216
  parent?: SchemaDataNode;
217
217
  getChild(schemaNode: SchemaNode): SchemaDataNode;
218
218
  getChildElement(index: number): SchemaDataNode;
@@ -221,7 +221,7 @@ export declare function findField(fields: SchemaField[], field: string): SchemaF
221
221
  export declare function isScalarField(sf: SchemaField): sf is SchemaField;
222
222
  export declare function isCompoundField(sf: SchemaField): sf is CompoundField;
223
223
  export declare function createSchemaLookup<A extends Record<string, SchemaField[]>>(schemaMap: A): SchemaTreeLookup<keyof A>;
224
- export declare function makeSchemaDataNode(schema: SchemaNode, control?: Control<unknown>, parent?: SchemaDataNode, elementIndex?: number): SchemaDataNode;
224
+ export declare function makeSchemaDataNode(schema: SchemaNode, control: Control<unknown>, parent?: SchemaDataNode, elementIndex?: number): SchemaDataNode;
225
225
  export declare function schemaDataForFieldRef(fieldRef: string | undefined, schema: SchemaDataNode): SchemaDataNode;
226
226
  export declare function schemaForFieldRef(fieldRef: string | undefined, schema: SchemaNode): SchemaNode;
227
227
  export declare function traverseSchemaPath<A>(fieldPath: string[], schema: SchemaNode, acc: A, next: (acc: A, node: SchemaNode) => A): A;
@@ -243,7 +243,8 @@ export declare enum SchemaTags {
243
243
  NoControl = "_NoControl",
244
244
  HtmlEditor = "_HtmlEditor",
245
245
  ControlGroup = "_ControlGroup:",
246
- ControlRef = "_ControlRef:"
246
+ ControlRef = "_ControlRef:",
247
+ IdField = "_IdField:"
247
248
  }
248
249
  export declare function getTagParam(field: SchemaField, tag: string): string | undefined;
249
250
  export declare function makeParamTag(tag: string, value: string): string;
package/lib/util.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ControlActionHandler, ControlDefinition, DataControlDefinition, DisplayOnlyRenderOptions, GroupRenderOptions } from "./controlDefinition";
2
2
  import { MutableRefObject } from "react";
3
- import { CompoundField, FieldOption, SchemaField, SchemaNode } from "./schemaField";
3
+ import { CompoundField, FieldOption, SchemaDataNode, SchemaField, SchemaNode } from "./schemaField";
4
4
  /**
5
5
  * Interface representing the classes for a control.
6
6
  */
@@ -243,3 +243,4 @@ export declare function isControlDisplayOnly(def: ControlDefinition): boolean;
243
243
  * @returns {ControlActionHandler} - The combined action handler.
244
244
  */
245
245
  export declare function actionHandlers(...handlers: (ControlActionHandler | undefined)[]): ControlActionHandler;
246
+ export declare function getDiffObject(dataNode: SchemaDataNode, idField?: string | null): any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-typed-forms/schemas",
3
- "version": "14.0.3",
3
+ "version": "14.0.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.cjs",
@@ -42,6 +42,11 @@
42
42
  "devDependencies": {
43
43
  "react": "^18.2.0",
44
44
  "@react-typed-forms/transform": "^0.2.0",
45
+ "jest": "^29.7.0",
46
+ "tsx": "^4.19.1",
47
+ "fast-check": "^3.22.0",
48
+ "ts-jest": "^29.2.5",
49
+ "@jest/globals": "^29.7.0",
45
50
  "typedoc": "^0.27.2",
46
51
  "@types/uuid": "^10.0.0",
47
52
  "@types/react": "^18.2.28",
@@ -51,12 +56,13 @@
51
56
  "prettier": "^3.0.3",
52
57
  "rimraf": "^3.0.2",
53
58
  "typescript": "^5.6.2",
54
- "@react-typed-forms/core": "4.0.0"
59
+ "@react-typed-forms/core": "4.0.3"
55
60
  },
56
61
  "gitHead": "698e16cd3ab31b7dd0528fc76536f4d3205ce8c6",
57
62
  "scripts": {
58
63
  "build": "rimraf ./lib/ node_modules/.cache && microbundle -f modern,cjs --jsx React.createElement --jsxFragment React.Fragment",
59
64
  "watch": "microbundle -w -f modern,cjs --no-compress --jsx React.createElement --jsxFragment React.Fragment",
65
+ "test": "jest --coverage",
60
66
  "update-readme": "md-magic --path README.md",
61
67
  "gencode": "nswag swagger2tsclient /input:http://localhost:5216/swagger/v1/swagger.json /runtime:Net60 /output:src/types.ts /GenerateClientClasses:false /MarkOptionalProperties:false /Template:Fetch /TypeStyle:Interface /DateTimeType:string"
62
68
  }
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import fc from "fast-check";
3
+ import { changeValue, makeDataNode, valueAndSchema } from "./gen";
4
+ import {
5
+ getDiffObject,
6
+ getTagParam,
7
+ isCompoundNode,
8
+ SchemaDataNode,
9
+ SchemaTags,
10
+ } from "../src";
11
+ import { Control } from "@react-typed-forms/core";
12
+
13
+ describe("diff", () => {
14
+ it("unchanged value always returns undefined", () => {
15
+ fc.assert(
16
+ fc.property(valueAndSchema(), (fv) => {
17
+ const dataNode = makeDataNode(fv);
18
+ expect(getDiffObject(dataNode)).toBeUndefined();
19
+ }),
20
+ );
21
+ });
22
+
23
+ it("primitive value always returns new value", () => {
24
+ fc.assert(
25
+ fc.property(
26
+ valueAndSchema({ arrayChance: 0, compoundChance: 0 }),
27
+ (fv) => {
28
+ const dataNode = makeDataNode(fv);
29
+ const control = dataNode.control!;
30
+ control.setValue((x) => changeValue(x, dataNode.schema.field));
31
+ expect(getDiffObject(dataNode)).toBe(control.value);
32
+ },
33
+ ),
34
+ );
35
+ });
36
+
37
+ it("compound fields only return changed values", () => {
38
+ fc.assert(
39
+ fc.property(
40
+ valueAndSchema({ arrayChance: 0, forceCompound: true }),
41
+ (fv) => {
42
+ const dataNode = makeDataNode(fv);
43
+ const control = dataNode.control!;
44
+ const result = { ...control.value };
45
+ const newValue = { ...control.value };
46
+ dataNode.schema.getChildNodes().forEach((child, i) => {
47
+ const field = child.field;
48
+ const fieldName = field.field;
49
+ if (i % 2 == 0) {
50
+ const nv = changeValue(newValue[fieldName], field);
51
+ newValue[fieldName] = nv;
52
+ result[fieldName] = nv;
53
+ } else {
54
+ delete result[fieldName];
55
+ }
56
+ });
57
+ control.value = newValue;
58
+ expect(getDiffObject(dataNode)).toStrictEqual(result);
59
+ },
60
+ ),
61
+ );
62
+ });
63
+
64
+ it("array compound with id field always returns id field and changes", () => {
65
+ fc.assert(
66
+ fc.property(
67
+ valueAndSchema({
68
+ forceArray: true,
69
+ forceCompound: true,
70
+ arrayChance: 0,
71
+ compoundChance: 0,
72
+ idField: true,
73
+ }),
74
+ (fv) => {
75
+ const dataNode = makeDataNode(fv);
76
+ const arrayControl = dataNode.control!;
77
+ let results: any = undefined;
78
+ arrayControl.as<any[]>().elements.forEach((control) => {
79
+ const { newValue, result } = editCompound(control.value, dataNode);
80
+ control.value = newValue;
81
+ if (!results) results = [];
82
+ results.push(result);
83
+ });
84
+ expect(getDiffObject(dataNode)).toStrictEqual(results);
85
+ },
86
+ ),
87
+ );
88
+ });
89
+
90
+ it("array without id always returns array index edit format", () => {
91
+ fc.assert(
92
+ fc.property(
93
+ valueAndSchema({
94
+ forceArray: true,
95
+ forceCompound: true,
96
+ arrayChance: 0,
97
+ compoundChance: 0,
98
+ }).chain((fv) => {
99
+ const len = ((fv.value as any[]) ?? []).length;
100
+ return fc
101
+ .record({
102
+ index: fc.integer({ min: 0, max: len }),
103
+ add: len ? fc.boolean() : fc.constant(true),
104
+ })
105
+ .map((x) => ({ ...fv, ...x }));
106
+ }),
107
+ (fv) => {
108
+ const dataNode = makeDataNode(fv);
109
+ const arrayControl = dataNode.control!.as<any[]>();
110
+ let results: any = undefined;
111
+ // if (fv.add || fv.index >= arrayControl.elements.length)
112
+ // addElement(
113
+ // arrayControl,
114
+ // changeValue(undefined, dataNode.schema.field, true),
115
+ // );
116
+ arrayControl.as<any[]>().elements.forEach((control, i) => {
117
+ let change = undefined;
118
+ if (i % 2 == 0) {
119
+ if (isCompoundNode(dataNode.schema)) {
120
+ const { newValue, result } = editCompound(
121
+ control.value,
122
+ dataNode,
123
+ );
124
+ control.value = newValue;
125
+ change = result;
126
+ } else {
127
+ change = changeValue(control.value, dataNode.schema.field);
128
+ control.value = change;
129
+ }
130
+ }
131
+ if (!results) results = [];
132
+ results.push({
133
+ old: i,
134
+ edit: change,
135
+ });
136
+ });
137
+ expect(getDiffObject(dataNode)).toStrictEqual(results);
138
+ },
139
+ ),
140
+ );
141
+ });
142
+ });
143
+
144
+ function editCompound(
145
+ existing: Record<string, any>,
146
+ dataNode: SchemaDataNode,
147
+ ): { newValue: any; result: any } {
148
+ const idField = getTagParam(dataNode.schema.field, SchemaTags.IdField);
149
+ const result = { ...existing };
150
+ const newValue = { ...existing };
151
+ dataNode.schema.getChildNodes().forEach((child, i) => {
152
+ const field = child.field;
153
+ const fieldName = field.field;
154
+ const shouldChange = i % 2 == 0;
155
+ if (shouldChange || fieldName == idField) {
156
+ let nv = newValue[fieldName];
157
+ if (shouldChange) nv = changeValue(nv, field);
158
+ if (nv === undefined) nv = null;
159
+ newValue[fieldName] = nv;
160
+ result[fieldName] = nv;
161
+ } else {
162
+ delete result[fieldName];
163
+ }
164
+ });
165
+ return { newValue, result };
166
+ }
package/test/gen.ts ADDED
@@ -0,0 +1,175 @@
1
+ import fc, { Arbitrary } from "fast-check";
2
+ import {
3
+ CompoundField,
4
+ createSchemaLookup,
5
+ FieldType,
6
+ isCompoundField,
7
+ makeSchemaDataNode,
8
+ SchemaDataNode,
9
+ SchemaField,
10
+ SchemaTags,
11
+ } from "../src";
12
+ import { newControl } from "@react-typed-forms/core";
13
+
14
+ export interface FieldAndValue {
15
+ field: SchemaField;
16
+ value: any;
17
+ }
18
+
19
+ export function valueAndSchema(
20
+ options?: SchemaFieldGenOptions,
21
+ ): Arbitrary<FieldAndValue> {
22
+ return randomSchemaField(options).chain((schema) =>
23
+ randomValueForField(schema).map((value) => ({ field: schema, value })),
24
+ );
25
+ }
26
+
27
+ export function makeDataNode(fv: FieldAndValue): SchemaDataNode {
28
+ return makeSchemaDataNode(
29
+ createSchemaLookup({ "": [fv.field] })
30
+ .getSchema("")!
31
+ .getChildNode(fv.field.field)!,
32
+ newControl(fv.value),
33
+ );
34
+ }
35
+
36
+ export interface SchemaFieldGenOptions {
37
+ arrayChance?: number;
38
+ forceCompound?: boolean;
39
+ forceArray?: boolean;
40
+ compoundChance?: number;
41
+ idField?: boolean;
42
+ }
43
+ function randomSchemaField(
44
+ options: SchemaFieldGenOptions = {},
45
+ ): Arbitrary<SchemaField> {
46
+ const {
47
+ arrayChance = 5,
48
+ compoundChance = 10,
49
+ forceCompound,
50
+ forceArray,
51
+ idField,
52
+ } = options;
53
+ const nextOptions = { arrayChance, compoundChance };
54
+ const field = fc.oneof(
55
+ {
56
+ weight: forceCompound ? 100 : compoundChance,
57
+ arbitrary: fc.constant(FieldType.Compound),
58
+ },
59
+ {
60
+ weight: forceCompound ? 0 : 100 - compoundChance,
61
+ arbitrary: fc.constantFrom(
62
+ FieldType.String,
63
+ FieldType.Int,
64
+ FieldType.Double,
65
+ FieldType.Bool,
66
+ FieldType.Date,
67
+ FieldType.DateTime,
68
+ FieldType.Time,
69
+ ),
70
+ },
71
+ );
72
+ const collection = fc.oneof(
73
+ {
74
+ weight: forceArray ? 0 : 100 - arrayChance,
75
+ arbitrary: fc.constant(false),
76
+ },
77
+ {
78
+ weight: forceArray ? 100 : arrayChance,
79
+ arbitrary: fc.constant(true),
80
+ },
81
+ );
82
+
83
+ const withoutId = field.chain((fieldType) =>
84
+ fc.record({
85
+ field: fc.string(),
86
+ type: fc.constant(fieldType),
87
+ collection,
88
+ notNullable: fc.boolean(),
89
+ children:
90
+ fieldType == FieldType.Compound
91
+ ? fc
92
+ .array(randomSchemaField(nextOptions), {
93
+ minLength: 1,
94
+ maxLength: 10,
95
+ })
96
+ .map((x) =>
97
+ Object.values(Object.fromEntries(x.map((y) => [y.field, y]))),
98
+ )
99
+ : fc.constant(null),
100
+ }),
101
+ );
102
+ return !idField
103
+ ? withoutId
104
+ : withoutId.chain((x) =>
105
+ fc.integer({ min: 0, max: (x.children?.length ?? 0) - 1 }).map((i) => ({
106
+ ...x,
107
+ tags: [SchemaTags.IdField + x.children![i].field],
108
+ })),
109
+ );
110
+ }
111
+ function randomValueForField(
112
+ f: SchemaField,
113
+ element?: boolean,
114
+ ): Arbitrary<any> {
115
+ return fc.integer({ min: 0, max: 100 }).chain(((nc) => {
116
+ if (nc <= 75 || f.notNullable) {
117
+ if (!element && f.collection) {
118
+ return fc.array(randomValueForField(f, true), {
119
+ minLength: 0,
120
+ maxLength: 10,
121
+ });
122
+ }
123
+ if (f.type === FieldType.String) return fc.string();
124
+ if (f.type === FieldType.Int) return fc.integer();
125
+ if (f.type === FieldType.Double) return fc.double();
126
+ if (f.type === FieldType.Bool) return fc.boolean();
127
+ if (f.type === FieldType.Date)
128
+ return fc.date().map((x) => x.toISOString().substring(0, 10));
129
+ if (f.type === FieldType.DateTime)
130
+ return fc.date().map((x) => x.toISOString());
131
+ if (f.type === FieldType.Time)
132
+ return fc.date().map((x) => x.toISOString().substring(11));
133
+ if (isCompoundField(f))
134
+ return fc.record(
135
+ Object.fromEntries(
136
+ f.children.map((x) => [x.field, randomValueForField(x)]),
137
+ ),
138
+ );
139
+ }
140
+ return fc.constantFrom(null, undefined);
141
+ }) as (x: number) => Arbitrary<any>);
142
+ }
143
+
144
+ export function changeValue(
145
+ value: any,
146
+ field: SchemaField,
147
+ element?: boolean,
148
+ ): any {
149
+ if (field.collection && !element) {
150
+ return [...(value ?? []), changeValue(undefined, field, true)];
151
+ }
152
+ switch (field.type) {
153
+ case FieldType.Compound:
154
+ const objValue = value ?? {};
155
+ return Object.fromEntries(
156
+ (field as CompoundField).children.map((x) => [
157
+ x.field,
158
+ changeValue(objValue[x.field], x),
159
+ ]),
160
+ );
161
+ case FieldType.String:
162
+ case FieldType.Date:
163
+ case FieldType.DateTime:
164
+ case FieldType.Time:
165
+ return (value ?? "") + "x";
166
+ case FieldType.Int:
167
+ const v = value ?? 0;
168
+ return !v ? 1 : -v;
169
+ case FieldType.Double:
170
+ const dv = value ?? 0;
171
+ return !dv ? 1 : -dv;
172
+ case FieldType.Bool:
173
+ return !(value ?? false);
174
+ }
175
+ }
package/test/play.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { changeValue, FieldAndValue, makeDataNode } from "./gen";
2
+ import { getDiffObject } from "../src";
3
+
4
+
5
+ const fv = {
6
+ field: {
7
+ field: "",
8
+ type: "Compound",
9
+ collection: true,
10
+ notNullable: false,
11
+ children: [
12
+ {
13
+ field: "",
14
+ type: "String",
15
+ collection: false,
16
+ notNullable: false,
17
+ children: null,
18
+ },
19
+ ],
20
+ },
21
+ value: [{ "": "" }],
22
+ index: 0,
23
+ add: false,
24
+ } as FieldAndValue;
25
+
26
+ const dataNode = makeDataNode(fv);
27
+ const arrayControl = dataNode.control!;
28
+ let results: any = undefined;
29
+ arrayControl.as<any[]>().elements.forEach((control) => {
30
+ const result = { ...control.value };
31
+ const newValue = { ...control.value };
32
+ dataNode.schema.getChildNodes().forEach((child, i) => {
33
+ const field = child.field;
34
+ const fieldName = field.field;
35
+ if (i % 2 == 0) {
36
+ const nv = changeValue(newValue[fieldName], field);
37
+ newValue[fieldName] = nv;
38
+ result[fieldName] = nv;
39
+ } else {
40
+ delete result[fieldName];
41
+ }
42
+ });
43
+ control.value = newValue;
44
+ if (!results) results = [];
45
+ results.push(result);
46
+ });
47
+ console.log(fv.value);
48
+ console.log(arrayControl.value);
49
+ console.log(results);
50
+ console.log(getDiffObject(dataNode));