@narrative.io/jsonforms-provider-protocols 3.0.0-beta.12 → 3.0.0-beta.14
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.
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* Initialize a form data object from a JSON Schema.
|
|
3
3
|
* Resolves $ref, const, default, oneOf/discriminator, and typed empty values.
|
|
4
4
|
*
|
|
5
|
+
* Optional fields (not listed in the parent schema's `required` array) that
|
|
6
|
+
* have no `const`, no single-value `enum`, and no `default` are omitted
|
|
7
|
+
* entirely from the result. This avoids seeding values (e.g. `null` for
|
|
8
|
+
* `type: "integer"`) that fail AJV's type check and surface spurious errors
|
|
9
|
+
* on untouched fields. Required fields retain legacy typed-empty seeding
|
|
10
|
+
* (`""`, `null`, `false`, `[]`) so that "is required" surfaces cleanly.
|
|
11
|
+
*
|
|
5
12
|
* @param schema - The full JSON Schema (must include $defs if $refs are used)
|
|
6
13
|
* @param seed - Optional existing data to merge (seed values take priority)
|
|
7
|
-
* @returns A data object with
|
|
14
|
+
* @returns A data object with schema-defined fields initialized
|
|
8
15
|
*/
|
|
9
16
|
export declare function initFormDataFromSchema(schema: Record<string, unknown>, seed?: Record<string, unknown>): Record<string, unknown>;
|
|
10
17
|
//# sourceMappingURL=initFormData.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initFormData.d.ts","sourceRoot":"","sources":["../../src/core/initFormData.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"initFormData.d.ts","sourceRoot":"","sources":["../../src/core/initFormData.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB"}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
function initFormDataFromSchema(schema, seed) {
|
|
2
|
-
const result = initProperty(schema, schema, seed);
|
|
3
|
-
|
|
2
|
+
const result = initProperty(schema, schema, seed, true);
|
|
3
|
+
const base = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
4
|
+
if (seed && typeof seed === "object") {
|
|
4
5
|
const schemaKeys = new Set(
|
|
5
6
|
Object.keys(
|
|
6
7
|
resolveRef(schema, schema)?.properties ?? {}
|
|
7
8
|
)
|
|
8
9
|
);
|
|
9
10
|
for (const key of Object.keys(seed)) {
|
|
10
|
-
if (!schemaKeys.has(key) && !(key in
|
|
11
|
-
|
|
11
|
+
if (!schemaKeys.has(key) && !(key in base)) {
|
|
12
|
+
base[key] = seed[key];
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
return
|
|
16
|
+
return base;
|
|
16
17
|
}
|
|
17
18
|
function resolveRef(property, root, seen) {
|
|
18
19
|
if (!property || typeof property !== "object") return property;
|
|
@@ -38,14 +39,14 @@ function resolvePointer(obj, pointer) {
|
|
|
38
39
|
}
|
|
39
40
|
return current;
|
|
40
41
|
}
|
|
41
|
-
function initProperty(property, root, seed) {
|
|
42
|
-
if (!property || typeof property !== "object")
|
|
43
|
-
|
|
44
|
-
if (Array.isArray(resolved.oneOf)) {
|
|
45
|
-
return initOneOf(resolved, root, seed);
|
|
42
|
+
function initProperty(property, root, seed, required) {
|
|
43
|
+
if (!property || typeof property !== "object") {
|
|
44
|
+
return required ? null : void 0;
|
|
46
45
|
}
|
|
46
|
+
const resolved = resolveRef(property, root);
|
|
47
47
|
const type = resolved.type;
|
|
48
|
-
|
|
48
|
+
const isOneOf = Array.isArray(resolved.oneOf);
|
|
49
|
+
if (seed !== void 0 && seed !== null && type !== "object" && !isOneOf) {
|
|
49
50
|
return seed;
|
|
50
51
|
}
|
|
51
52
|
if ("const" in resolved) {
|
|
@@ -57,9 +58,25 @@ function initProperty(property, root, seed) {
|
|
|
57
58
|
if ("default" in resolved) {
|
|
58
59
|
return resolved.default;
|
|
59
60
|
}
|
|
61
|
+
if (isOneOf) {
|
|
62
|
+
const value = initOneOf(resolved, root, seed, required);
|
|
63
|
+
if (!required && (value === void 0 || value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)) {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
if (type === "object") {
|
|
69
|
+
const obj = initObject(
|
|
70
|
+
resolved,
|
|
71
|
+
root,
|
|
72
|
+
seed,
|
|
73
|
+
required
|
|
74
|
+
);
|
|
75
|
+
if (!required && Object.keys(obj).length === 0) return void 0;
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
78
|
+
if (!required) return void 0;
|
|
60
79
|
switch (type) {
|
|
61
|
-
case "object":
|
|
62
|
-
return initObject(resolved, root, seed);
|
|
63
80
|
case "array":
|
|
64
81
|
return seed !== void 0 && seed !== null ? seed : [];
|
|
65
82
|
case "string":
|
|
@@ -68,30 +85,36 @@ function initProperty(property, root, seed) {
|
|
|
68
85
|
return false;
|
|
69
86
|
case "number":
|
|
70
87
|
case "integer":
|
|
71
|
-
return null;
|
|
72
88
|
default:
|
|
73
89
|
return null;
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
|
-
function initObject(schema, root, seed) {
|
|
92
|
+
function initObject(schema, root, seed, parentRequired) {
|
|
77
93
|
const properties = schema.properties;
|
|
78
94
|
if (!properties) {
|
|
79
95
|
return seed && typeof seed === "object" ? { ...seed } : {};
|
|
80
96
|
}
|
|
97
|
+
const requiredSet = new Set(
|
|
98
|
+
Array.isArray(schema.required) ? schema.required : []
|
|
99
|
+
);
|
|
81
100
|
const result = {};
|
|
82
101
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
83
102
|
const seedValue = seed && typeof seed === "object" ? seed[key] : void 0;
|
|
84
|
-
|
|
103
|
+
const effectiveRequired = parentRequired && requiredSet.has(key);
|
|
104
|
+
const value = initProperty(propSchema, root, seedValue, effectiveRequired);
|
|
105
|
+
if (value !== void 0) {
|
|
106
|
+
result[key] = value;
|
|
107
|
+
}
|
|
85
108
|
}
|
|
86
109
|
return result;
|
|
87
110
|
}
|
|
88
|
-
function initOneOf(schema, root, seed) {
|
|
111
|
+
function initOneOf(schema, root, seed, required) {
|
|
89
112
|
const variants = schema.oneOf;
|
|
90
|
-
if (!variants || variants.length === 0) return null;
|
|
113
|
+
if (!variants || variants.length === 0) return required ? null : void 0;
|
|
91
114
|
const first = variants[0];
|
|
92
|
-
if (!first) return null;
|
|
115
|
+
if (!first) return required ? null : void 0;
|
|
93
116
|
const firstVariant = resolveRef(first, root);
|
|
94
|
-
return initProperty(firstVariant, root, seed);
|
|
117
|
+
return initProperty(firstVariant, root, seed, required);
|
|
95
118
|
}
|
|
96
119
|
export {
|
|
97
120
|
initFormDataFromSchema
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initFormData.js","sources":["../../src/core/initFormData.ts"],"sourcesContent":["/**\n * Initialize a form data object from a JSON Schema.\n * Resolves $ref, const, default, oneOf/discriminator, and typed empty values.\n *\n * @param schema - The full JSON Schema (must include $defs if $refs are used)\n * @param seed - Optional existing data to merge (seed values take priority)\n * @returns A data object with all schema-defined fields initialized\n */\nexport function initFormDataFromSchema(\n schema: Record<string, unknown>,\n seed?: Record<string, unknown>,\n): Record<string, unknown> {\n const result = initProperty(schema, schema, seed) as Record<string, unknown>;\n\n // If result is an object and seed has extra keys not in schema, preserve them\n if (\n result &&\n typeof result === \"object\" &&\n !Array.isArray(result) &&\n seed &&\n typeof seed === \"object\"\n ) {\n const schemaKeys = new Set(\n Object.keys(\n (resolveRef(schema, schema) as Record<string, unknown>)?.properties ??\n {},\n ),\n );\n for (const key of Object.keys(seed)) {\n if (!schemaKeys.has(key) && !(key in result)) {\n result[key] = seed[key];\n }\n }\n }\n\n return result ?? {};\n}\n\n/**\n * Resolve a $ref pointer against the root schema's $defs.\n * Supports nested $ref chains.\n */\nfunction resolveRef(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seen?: Set<string>,\n): Record<string, unknown> {\n if (!property || typeof property !== \"object\") return property;\n\n const ref = property.$ref as string | undefined;\n if (!ref) return property;\n\n // Guard against circular refs\n const visited = seen ?? new Set<string>();\n if (visited.has(ref)) return property;\n visited.add(ref);\n\n const resolved = resolvePointer(root, ref);\n if (!resolved) return property;\n\n // Continue resolving if the result itself has a $ref\n return resolveRef(resolved as Record<string, unknown>, root, visited);\n}\n\n/**\n * Resolve a JSON pointer like \"#/$defs/Price\" against an object.\n */\nfunction resolvePointer(\n obj: Record<string, unknown>,\n pointer: string,\n): unknown {\n if (!pointer.startsWith(\"#/\")) return undefined;\n const parts = pointer.slice(2).split(\"/\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current && typeof current === \"object\" && part in current) {\n current = (current as Record<string, unknown>)[part];\n } else {\n return undefined;\n }\n }\n return current;\n}\n\n/**\n * Initialize a single property value based on its schema definition.\n */\nfunction initProperty(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seed?: unknown,\n): unknown {\n if (!property || typeof property !== \"object\") return null;\n\n // Resolve $ref first\n const resolved = resolveRef(property, root);\n\n // Handle oneOf with discriminator — pick first variant\n if (Array.isArray(resolved.oneOf)) {\n return initOneOf(resolved, root, seed);\n }\n\n // Priority 1: seed wins (for object types, we merge recursively below)\n // For non-object types, return seed directly if present\n const type = resolved.type as string | undefined;\n if (seed !== undefined && seed !== null && type !== \"object\") {\n return seed;\n }\n\n // Priority 2: const\n if (\"const\" in resolved) {\n return resolved.const;\n }\n\n // Priority 3: single-value enum\n if (\n Array.isArray(resolved.enum) &&\n (resolved.enum as unknown[]).length === 1\n ) {\n return (resolved.enum as unknown[])[0];\n }\n\n // Priority 4: default\n if (\"default\" in resolved) {\n return resolved.default;\n }\n\n // Priority 5: typed empty values\n switch (type) {\n case \"object\":\n return initObject(resolved, root, seed as Record<string, unknown>);\n case \"array\":\n return seed !== undefined && seed !== null ? seed : [];\n case \"string\":\n return \"\";\n case \"boolean\":\n return false;\n case \"number\":\n case \"integer\":\n return null;\n default:\n return null;\n }\n}\n\n/**\n * Initialize an object type by recursing into its properties.\n */\nfunction initObject(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed?: Record<string, unknown>,\n): Record<string, unknown> {\n const properties = schema.properties as\n | Record<string, Record<string, unknown>>\n | undefined;\n if (!properties) {\n // Object with no defined properties — return seed or empty object\n return seed && typeof seed === \"object\" ? { ...seed } : {};\n }\n\n const result: Record<string, unknown> = {};\n\n for (const [key, propSchema] of Object.entries(properties)) {\n const seedValue = seed && typeof seed === \"object\" ? seed[key] : undefined;\n result[key] = initProperty(propSchema, root, seedValue);\n }\n\n return result;\n}\n\n/**\n * Handle oneOf schemas — pick the first variant and initialize it.\n */\nfunction initOneOf(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed?: unknown,\n): unknown {\n const variants = schema.oneOf as Record<string, unknown>[];\n if (!variants || variants.length === 0) return null;\n\n const first = variants[0];\n if (!first) return null;\n\n // Pick first variant, resolve its $ref if needed\n const firstVariant = resolveRef(first, root);\n return initProperty(firstVariant, root, seed);\n}\n"],"names":[],"mappings":"AAQO,SAAS,uBACd,QACA,MACyB;AACzB,QAAM,SAAS,aAAa,QAAQ,QAAQ,IAAI;AAGhD,MACE,UACA,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,MAAM,KACrB,QACA,OAAO,SAAS,UAChB;AACA,UAAM,aAAa,IAAI;AAAA,MACrB,OAAO;AAAA,QACJ,WAAW,QAAQ,MAAM,GAA+B,cACvD,CAAA;AAAA,MAAC;AAAA,IACL;AAEF,eAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACnC,UAAI,CAAC,WAAW,IAAI,GAAG,KAAK,EAAE,OAAO,SAAS;AAC5C,eAAO,GAAG,IAAI,KAAK,GAAG;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,UAAU,CAAA;AACnB;AAMA,SAAS,WACP,UACA,MACA,MACyB;AACzB,MAAI,CAAC,YAAY,OAAO,aAAa,SAAU,QAAO;AAEtD,QAAM,MAAM,SAAS;AACrB,MAAI,CAAC,IAAK,QAAO;AAGjB,QAAM,UAAU,QAAQ,oBAAI,IAAA;AAC5B,MAAI,QAAQ,IAAI,GAAG,EAAG,QAAO;AAC7B,UAAQ,IAAI,GAAG;AAEf,QAAM,WAAW,eAAe,MAAM,GAAG;AACzC,MAAI,CAAC,SAAU,QAAO;AAGtB,SAAO,WAAW,UAAqC,MAAM,OAAO;AACtE;AAKA,SAAS,eACP,KACA,SACS;AACT,MAAI,CAAC,QAAQ,WAAW,IAAI,EAAG,QAAO;AACtC,QAAM,QAAQ,QAAQ,MAAM,CAAC,EAAE,MAAM,GAAG;AACxC,MAAI,UAAmB;AACvB,aAAW,QAAQ,OAAO;AACxB,QAAI,WAAW,OAAO,YAAY,YAAY,QAAQ,SAAS;AAC7D,gBAAW,QAAoC,IAAI;AAAA,IACrD,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,aACP,UACA,MACA,MACS;AACT,MAAI,CAAC,YAAY,OAAO,aAAa,SAAU,QAAO;AAGtD,QAAM,WAAW,WAAW,UAAU,IAAI;AAG1C,MAAI,MAAM,QAAQ,SAAS,KAAK,GAAG;AACjC,WAAO,UAAU,UAAU,MAAM,IAAI;AAAA,EACvC;AAIA,QAAM,OAAO,SAAS;AACtB,MAAI,SAAS,UAAa,SAAS,QAAQ,SAAS,UAAU;AAC5D,WAAO;AAAA,EACT;AAGA,MAAI,WAAW,UAAU;AACvB,WAAO,SAAS;AAAA,EAClB;AAGA,MACE,MAAM,QAAQ,SAAS,IAAI,KAC1B,SAAS,KAAmB,WAAW,GACxC;AACA,WAAQ,SAAS,KAAmB,CAAC;AAAA,EACvC;AAGA,MAAI,aAAa,UAAU;AACzB,WAAO,SAAS;AAAA,EAClB;AAGA,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO,WAAW,UAAU,MAAM,IAA+B;AAAA,IACnE,KAAK;AACH,aAAO,SAAS,UAAa,SAAS,OAAO,OAAO,CAAA;AAAA,IACtD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EAAA;AAEb;AAKA,SAAS,WACP,QACA,MACA,MACyB;AACzB,QAAM,aAAa,OAAO;AAG1B,MAAI,CAAC,YAAY;AAEf,WAAO,QAAQ,OAAO,SAAS,WAAW,EAAE,GAAG,KAAA,IAAS,CAAA;AAAA,EAC1D;AAEA,QAAM,SAAkC,CAAA;AAExC,aAAW,CAAC,KAAK,UAAU,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC1D,UAAM,YAAY,QAAQ,OAAO,SAAS,WAAW,KAAK,GAAG,IAAI;AACjE,WAAO,GAAG,IAAI,aAAa,YAAY,MAAM,SAAS;AAAA,EACxD;AAEA,SAAO;AACT;AAKA,SAAS,UACP,QACA,MACA,MACS;AACT,QAAM,WAAW,OAAO;AACxB,MAAI,CAAC,YAAY,SAAS,WAAW,EAAG,QAAO;AAE/C,QAAM,QAAQ,SAAS,CAAC;AACxB,MAAI,CAAC,MAAO,QAAO;AAGnB,QAAM,eAAe,WAAW,OAAO,IAAI;AAC3C,SAAO,aAAa,cAAc,MAAM,IAAI;AAC9C;"}
|
|
1
|
+
{"version":3,"file":"initFormData.js","sources":["../../src/core/initFormData.ts"],"sourcesContent":["/**\n * Initialize a form data object from a JSON Schema.\n * Resolves $ref, const, default, oneOf/discriminator, and typed empty values.\n *\n * Optional fields (not listed in the parent schema's `required` array) that\n * have no `const`, no single-value `enum`, and no `default` are omitted\n * entirely from the result. This avoids seeding values (e.g. `null` for\n * `type: \"integer\"`) that fail AJV's type check and surface spurious errors\n * on untouched fields. Required fields retain legacy typed-empty seeding\n * (`\"\"`, `null`, `false`, `[]`) so that \"is required\" surfaces cleanly.\n *\n * @param schema - The full JSON Schema (must include $defs if $refs are used)\n * @param seed - Optional existing data to merge (seed values take priority)\n * @returns A data object with schema-defined fields initialized\n */\nexport function initFormDataFromSchema(\n schema: Record<string, unknown>,\n seed?: Record<string, unknown>,\n): Record<string, unknown> {\n const result = initProperty(schema, schema, seed, true) as\n | Record<string, unknown>\n | undefined;\n\n const base =\n result && typeof result === \"object\" && !Array.isArray(result)\n ? result\n : {};\n\n // Preserve seed keys not described by the schema's properties.\n if (seed && typeof seed === \"object\") {\n const schemaKeys = new Set(\n Object.keys(\n (resolveRef(schema, schema) as Record<string, unknown>)?.properties ??\n {},\n ),\n );\n for (const key of Object.keys(seed)) {\n if (!schemaKeys.has(key) && !(key in base)) {\n base[key] = seed[key];\n }\n }\n }\n\n return base;\n}\n\n/**\n * Resolve a $ref pointer against the root schema's $defs.\n * Supports nested $ref chains.\n */\nfunction resolveRef(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seen?: Set<string>,\n): Record<string, unknown> {\n if (!property || typeof property !== \"object\") return property;\n\n const ref = property.$ref as string | undefined;\n if (!ref) return property;\n\n const visited = seen ?? new Set<string>();\n if (visited.has(ref)) return property;\n visited.add(ref);\n\n const resolved = resolvePointer(root, ref);\n if (!resolved) return property;\n\n return resolveRef(resolved as Record<string, unknown>, root, visited);\n}\n\n/**\n * Resolve a JSON pointer like \"#/$defs/Price\" against an object.\n */\nfunction resolvePointer(\n obj: Record<string, unknown>,\n pointer: string,\n): unknown {\n if (!pointer.startsWith(\"#/\")) return undefined;\n const parts = pointer.slice(2).split(\"/\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current && typeof current === \"object\" && part in current) {\n current = (current as Record<string, unknown>)[part];\n } else {\n return undefined;\n }\n }\n return current;\n}\n\n/**\n * Initialize a single property value based on its schema definition.\n * Returns `undefined` when the property is optional and has nothing\n * concrete to seed — the caller then omits the key from its result.\n */\nfunction initProperty(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seed: unknown,\n required: boolean,\n): unknown {\n if (!property || typeof property !== \"object\") {\n return required ? null : undefined;\n }\n\n const resolved = resolveRef(property, root);\n const type = resolved.type as string | undefined;\n const isOneOf = Array.isArray(resolved.oneOf);\n\n // Priority 1: seed wins for non-object, non-oneOf types. For oneOf we still\n // descend so that a partial seed (e.g. `{value: 5}` missing the\n // discriminator) picks up the variant's const/default for untouched fields.\n if (seed !== undefined && seed !== null && type !== \"object\" && !isOneOf) {\n return seed;\n }\n\n // Priority 2: const (schema-invariant — always set).\n if (\"const\" in resolved) {\n return resolved.const;\n }\n\n // Priority 3: single-value enum (same reasoning — only one valid value).\n if (\n Array.isArray(resolved.enum) &&\n (resolved.enum as unknown[]).length === 1\n ) {\n return (resolved.enum as unknown[])[0];\n }\n\n // Priority 4: default (explicit author intent — always honored).\n if (\"default\" in resolved) {\n return resolved.default;\n }\n\n // Priority 5: oneOf. Recurse into the first variant, propagating the\n // `required` flag so that the variant's own schema-declared required fields\n // only get typed-empty seeding when the container is required. For optional\n // containers, the variant's const / default / single-value enum still seed\n // (author intent is unconditional) but typed-empty placeholders do not.\n // If recursion yields nothing forced, we collapse back to undefined.\n if (isOneOf) {\n const value = initOneOf(resolved, root, seed, required);\n if (\n !required &&\n (value === undefined ||\n (value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0))\n ) {\n return undefined;\n }\n return value;\n }\n\n // Priority 6: objects recurse. The `required` flag propagates downward:\n // inside an optional ancestor, nested required primitives also collapse,\n // so an optional object with nested required fields stays absent instead\n // of materializing a shell that AJV would flag.\n if (type === \"object\") {\n const obj = initObject(\n resolved,\n root,\n seed as Record<string, unknown>,\n required,\n );\n if (!required && Object.keys(obj).length === 0) return undefined;\n return obj;\n }\n\n // Priority 7: optional primitives — omit.\n if (!required) return undefined;\n\n // Priority 8: required primitives — legacy typed empty.\n switch (type) {\n case \"array\":\n return seed !== undefined && seed !== null ? seed : [];\n case \"string\":\n return \"\";\n case \"boolean\":\n return false;\n case \"number\":\n case \"integer\":\n default:\n return null;\n }\n}\n\n/**\n * Initialize an object type by recursing into its properties.\n * Keys whose initialization returns `undefined` are omitted.\n *\n * `parentRequired` controls how schema.required propagates: a child is\n * treated as required only if both its parent is required AND it appears in\n * the parent's required array. Inside an optional ancestor the whole\n * subtree collapses (except for author-forced values: const, default,\n * single-value enum, or seeded values).\n */\nfunction initObject(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed: Record<string, unknown> | undefined,\n parentRequired: boolean,\n): Record<string, unknown> {\n const properties = schema.properties as\n | Record<string, Record<string, unknown>>\n | undefined;\n if (!properties) {\n return seed && typeof seed === \"object\" ? { ...seed } : {};\n }\n\n const requiredSet = new Set<string>(\n Array.isArray(schema.required) ? (schema.required as string[]) : [],\n );\n\n const result: Record<string, unknown> = {};\n\n for (const [key, propSchema] of Object.entries(properties)) {\n const seedValue = seed && typeof seed === \"object\" ? seed[key] : undefined;\n const effectiveRequired = parentRequired && requiredSet.has(key);\n const value = initProperty(propSchema, root, seedValue, effectiveRequired);\n if (value !== undefined) {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * Handle oneOf schemas — pick the first variant and initialize it.\n * `required` propagates: if the outer oneOf is required, the variant is\n * materialized with typed-empty seeding for its required fields; if optional,\n * only author-forced values (const / default / single-enum / seed) survive.\n */\nfunction initOneOf(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed: unknown,\n required: boolean,\n): unknown {\n const variants = schema.oneOf as Record<string, unknown>[];\n if (!variants || variants.length === 0) return required ? null : undefined;\n\n const first = variants[0];\n if (!first) return required ? null : undefined;\n\n const firstVariant = resolveRef(first, root);\n return initProperty(firstVariant, root, seed, required);\n}\n"],"names":[],"mappings":"AAeO,SAAS,uBACd,QACA,MACyB;AACzB,QAAM,SAAS,aAAa,QAAQ,QAAQ,MAAM,IAAI;AAItD,QAAM,OACJ,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,IACzD,SACA,CAAA;AAGN,MAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,UAAM,aAAa,IAAI;AAAA,MACrB,OAAO;AAAA,QACJ,WAAW,QAAQ,MAAM,GAA+B,cACvD,CAAA;AAAA,MAAC;AAAA,IACL;AAEF,eAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACnC,UAAI,CAAC,WAAW,IAAI,GAAG,KAAK,EAAE,OAAO,OAAO;AAC1C,aAAK,GAAG,IAAI,KAAK,GAAG;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,WACP,UACA,MACA,MACyB;AACzB,MAAI,CAAC,YAAY,OAAO,aAAa,SAAU,QAAO;AAEtD,QAAM,MAAM,SAAS;AACrB,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU,QAAQ,oBAAI,IAAA;AAC5B,MAAI,QAAQ,IAAI,GAAG,EAAG,QAAO;AAC7B,UAAQ,IAAI,GAAG;AAEf,QAAM,WAAW,eAAe,MAAM,GAAG;AACzC,MAAI,CAAC,SAAU,QAAO;AAEtB,SAAO,WAAW,UAAqC,MAAM,OAAO;AACtE;AAKA,SAAS,eACP,KACA,SACS;AACT,MAAI,CAAC,QAAQ,WAAW,IAAI,EAAG,QAAO;AACtC,QAAM,QAAQ,QAAQ,MAAM,CAAC,EAAE,MAAM,GAAG;AACxC,MAAI,UAAmB;AACvB,aAAW,QAAQ,OAAO;AACxB,QAAI,WAAW,OAAO,YAAY,YAAY,QAAQ,SAAS;AAC7D,gBAAW,QAAoC,IAAI;AAAA,IACrD,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAOA,SAAS,aACP,UACA,MACA,MACA,UACS;AACT,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC7C,WAAO,WAAW,OAAO;AAAA,EAC3B;AAEA,QAAM,WAAW,WAAW,UAAU,IAAI;AAC1C,QAAM,OAAO,SAAS;AACtB,QAAM,UAAU,MAAM,QAAQ,SAAS,KAAK;AAK5C,MAAI,SAAS,UAAa,SAAS,QAAQ,SAAS,YAAY,CAAC,SAAS;AACxE,WAAO;AAAA,EACT;AAGA,MAAI,WAAW,UAAU;AACvB,WAAO,SAAS;AAAA,EAClB;AAGA,MACE,MAAM,QAAQ,SAAS,IAAI,KAC1B,SAAS,KAAmB,WAAW,GACxC;AACA,WAAQ,SAAS,KAAmB,CAAC;AAAA,EACvC;AAGA,MAAI,aAAa,UAAU;AACzB,WAAO,SAAS;AAAA,EAClB;AAQA,MAAI,SAAS;AACX,UAAM,QAAQ,UAAU,UAAU,MAAM,MAAM,QAAQ;AACtD,QACE,CAAC,aACA,UAAU,UACR,UAAU,QACT,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,KAAK,KAAgC,EAAE,WAAW,IAC7D;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAMA,MAAI,SAAS,UAAU;AACrB,UAAM,MAAM;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAEF,QAAI,CAAC,YAAY,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AACvD,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,SAAU,QAAO;AAGtB,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO,SAAS,UAAa,SAAS,OAAO,OAAO,CAAA;AAAA,IACtD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EAAA;AAEb;AAYA,SAAS,WACP,QACA,MACA,MACA,gBACyB;AACzB,QAAM,aAAa,OAAO;AAG1B,MAAI,CAAC,YAAY;AACf,WAAO,QAAQ,OAAO,SAAS,WAAW,EAAE,GAAG,KAAA,IAAS,CAAA;AAAA,EAC1D;AAEA,QAAM,cAAc,IAAI;AAAA,IACtB,MAAM,QAAQ,OAAO,QAAQ,IAAK,OAAO,WAAwB,CAAA;AAAA,EAAC;AAGpE,QAAM,SAAkC,CAAA;AAExC,aAAW,CAAC,KAAK,UAAU,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC1D,UAAM,YAAY,QAAQ,OAAO,SAAS,WAAW,KAAK,GAAG,IAAI;AACjE,UAAM,oBAAoB,kBAAkB,YAAY,IAAI,GAAG;AAC/D,UAAM,QAAQ,aAAa,YAAY,MAAM,WAAW,iBAAiB;AACzE,QAAI,UAAU,QAAW;AACvB,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,SAAO;AACT;AAQA,SAAS,UACP,QACA,MACA,MACA,UACS;AACT,QAAM,WAAW,OAAO;AACxB,MAAI,CAAC,YAAY,SAAS,WAAW,EAAG,QAAO,WAAW,OAAO;AAEjE,QAAM,QAAQ,SAAS,CAAC;AACxB,MAAI,CAAC,MAAO,QAAO,WAAW,OAAO;AAErC,QAAM,eAAe,WAAW,OAAO,IAAI;AAC3C,SAAO,aAAa,cAAc,MAAM,MAAM,QAAQ;AACxD;"}
|
package/package.json
CHANGED
package/src/core/initFormData.ts
CHANGED
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
* Initialize a form data object from a JSON Schema.
|
|
3
3
|
* Resolves $ref, const, default, oneOf/discriminator, and typed empty values.
|
|
4
4
|
*
|
|
5
|
+
* Optional fields (not listed in the parent schema's `required` array) that
|
|
6
|
+
* have no `const`, no single-value `enum`, and no `default` are omitted
|
|
7
|
+
* entirely from the result. This avoids seeding values (e.g. `null` for
|
|
8
|
+
* `type: "integer"`) that fail AJV's type check and surface spurious errors
|
|
9
|
+
* on untouched fields. Required fields retain legacy typed-empty seeding
|
|
10
|
+
* (`""`, `null`, `false`, `[]`) so that "is required" surfaces cleanly.
|
|
11
|
+
*
|
|
5
12
|
* @param schema - The full JSON Schema (must include $defs if $refs are used)
|
|
6
13
|
* @param seed - Optional existing data to merge (seed values take priority)
|
|
7
|
-
* @returns A data object with
|
|
14
|
+
* @returns A data object with schema-defined fields initialized
|
|
8
15
|
*/
|
|
9
16
|
export function initFormDataFromSchema(
|
|
10
17
|
schema: Record<string, unknown>,
|
|
11
18
|
seed?: Record<string, unknown>,
|
|
12
19
|
): Record<string, unknown> {
|
|
13
|
-
const result = initProperty(schema, schema, seed) as
|
|
20
|
+
const result = initProperty(schema, schema, seed, true) as
|
|
21
|
+
| Record<string, unknown>
|
|
22
|
+
| undefined;
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
) {
|
|
24
|
+
const base =
|
|
25
|
+
result && typeof result === "object" && !Array.isArray(result)
|
|
26
|
+
? result
|
|
27
|
+
: {};
|
|
28
|
+
|
|
29
|
+
// Preserve seed keys not described by the schema's properties.
|
|
30
|
+
if (seed && typeof seed === "object") {
|
|
23
31
|
const schemaKeys = new Set(
|
|
24
32
|
Object.keys(
|
|
25
33
|
(resolveRef(schema, schema) as Record<string, unknown>)?.properties ??
|
|
@@ -27,13 +35,13 @@ export function initFormDataFromSchema(
|
|
|
27
35
|
),
|
|
28
36
|
);
|
|
29
37
|
for (const key of Object.keys(seed)) {
|
|
30
|
-
if (!schemaKeys.has(key) && !(key in
|
|
31
|
-
|
|
38
|
+
if (!schemaKeys.has(key) && !(key in base)) {
|
|
39
|
+
base[key] = seed[key];
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
return
|
|
44
|
+
return base;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/**
|
|
@@ -50,7 +58,6 @@ function resolveRef(
|
|
|
50
58
|
const ref = property.$ref as string | undefined;
|
|
51
59
|
if (!ref) return property;
|
|
52
60
|
|
|
53
|
-
// Guard against circular refs
|
|
54
61
|
const visited = seen ?? new Set<string>();
|
|
55
62
|
if (visited.has(ref)) return property;
|
|
56
63
|
visited.add(ref);
|
|
@@ -58,7 +65,6 @@ function resolveRef(
|
|
|
58
65
|
const resolved = resolvePointer(root, ref);
|
|
59
66
|
if (!resolved) return property;
|
|
60
67
|
|
|
61
|
-
// Continue resolving if the result itself has a $ref
|
|
62
68
|
return resolveRef(resolved as Record<string, unknown>, root, visited);
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -84,35 +90,36 @@ function resolvePointer(
|
|
|
84
90
|
|
|
85
91
|
/**
|
|
86
92
|
* Initialize a single property value based on its schema definition.
|
|
93
|
+
* Returns `undefined` when the property is optional and has nothing
|
|
94
|
+
* concrete to seed — the caller then omits the key from its result.
|
|
87
95
|
*/
|
|
88
96
|
function initProperty(
|
|
89
97
|
property: Record<string, unknown>,
|
|
90
98
|
root: Record<string, unknown>,
|
|
91
|
-
seed
|
|
99
|
+
seed: unknown,
|
|
100
|
+
required: boolean,
|
|
92
101
|
): unknown {
|
|
93
|
-
if (!property || typeof property !== "object")
|
|
94
|
-
|
|
95
|
-
// Resolve $ref first
|
|
96
|
-
const resolved = resolveRef(property, root);
|
|
97
|
-
|
|
98
|
-
// Handle oneOf with discriminator — pick first variant
|
|
99
|
-
if (Array.isArray(resolved.oneOf)) {
|
|
100
|
-
return initOneOf(resolved, root, seed);
|
|
102
|
+
if (!property || typeof property !== "object") {
|
|
103
|
+
return required ? null : undefined;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
// For non-object types, return seed directly if present
|
|
106
|
+
const resolved = resolveRef(property, root);
|
|
105
107
|
const type = resolved.type as string | undefined;
|
|
106
|
-
|
|
108
|
+
const isOneOf = Array.isArray(resolved.oneOf);
|
|
109
|
+
|
|
110
|
+
// Priority 1: seed wins for non-object, non-oneOf types. For oneOf we still
|
|
111
|
+
// descend so that a partial seed (e.g. `{value: 5}` missing the
|
|
112
|
+
// discriminator) picks up the variant's const/default for untouched fields.
|
|
113
|
+
if (seed !== undefined && seed !== null && type !== "object" && !isOneOf) {
|
|
107
114
|
return seed;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
// Priority 2: const
|
|
117
|
+
// Priority 2: const (schema-invariant — always set).
|
|
111
118
|
if ("const" in resolved) {
|
|
112
119
|
return resolved.const;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
// Priority 3: single-value enum
|
|
122
|
+
// Priority 3: single-value enum (same reasoning — only one valid value).
|
|
116
123
|
if (
|
|
117
124
|
Array.isArray(resolved.enum) &&
|
|
118
125
|
(resolved.enum as unknown[]).length === 1
|
|
@@ -120,15 +127,52 @@ function initProperty(
|
|
|
120
127
|
return (resolved.enum as unknown[])[0];
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
// Priority 4: default
|
|
130
|
+
// Priority 4: default (explicit author intent — always honored).
|
|
124
131
|
if ("default" in resolved) {
|
|
125
132
|
return resolved.default;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
// Priority 5:
|
|
135
|
+
// Priority 5: oneOf. Recurse into the first variant, propagating the
|
|
136
|
+
// `required` flag so that the variant's own schema-declared required fields
|
|
137
|
+
// only get typed-empty seeding when the container is required. For optional
|
|
138
|
+
// containers, the variant's const / default / single-value enum still seed
|
|
139
|
+
// (author intent is unconditional) but typed-empty placeholders do not.
|
|
140
|
+
// If recursion yields nothing forced, we collapse back to undefined.
|
|
141
|
+
if (isOneOf) {
|
|
142
|
+
const value = initOneOf(resolved, root, seed, required);
|
|
143
|
+
if (
|
|
144
|
+
!required &&
|
|
145
|
+
(value === undefined ||
|
|
146
|
+
(value !== null &&
|
|
147
|
+
typeof value === "object" &&
|
|
148
|
+
!Array.isArray(value) &&
|
|
149
|
+
Object.keys(value as Record<string, unknown>).length === 0))
|
|
150
|
+
) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Priority 6: objects recurse. The `required` flag propagates downward:
|
|
157
|
+
// inside an optional ancestor, nested required primitives also collapse,
|
|
158
|
+
// so an optional object with nested required fields stays absent instead
|
|
159
|
+
// of materializing a shell that AJV would flag.
|
|
160
|
+
if (type === "object") {
|
|
161
|
+
const obj = initObject(
|
|
162
|
+
resolved,
|
|
163
|
+
root,
|
|
164
|
+
seed as Record<string, unknown>,
|
|
165
|
+
required,
|
|
166
|
+
);
|
|
167
|
+
if (!required && Object.keys(obj).length === 0) return undefined;
|
|
168
|
+
return obj;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Priority 7: optional primitives — omit.
|
|
172
|
+
if (!required) return undefined;
|
|
173
|
+
|
|
174
|
+
// Priority 8: required primitives — legacy typed empty.
|
|
129
175
|
switch (type) {
|
|
130
|
-
case "object":
|
|
131
|
-
return initObject(resolved, root, seed as Record<string, unknown>);
|
|
132
176
|
case "array":
|
|
133
177
|
return seed !== undefined && seed !== null ? seed : [];
|
|
134
178
|
case "string":
|
|
@@ -137,7 +181,6 @@ function initProperty(
|
|
|
137
181
|
return false;
|
|
138
182
|
case "number":
|
|
139
183
|
case "integer":
|
|
140
|
-
return null;
|
|
141
184
|
default:
|
|
142
185
|
return null;
|
|
143
186
|
}
|
|
@@ -145,25 +188,40 @@ function initProperty(
|
|
|
145
188
|
|
|
146
189
|
/**
|
|
147
190
|
* Initialize an object type by recursing into its properties.
|
|
191
|
+
* Keys whose initialization returns `undefined` are omitted.
|
|
192
|
+
*
|
|
193
|
+
* `parentRequired` controls how schema.required propagates: a child is
|
|
194
|
+
* treated as required only if both its parent is required AND it appears in
|
|
195
|
+
* the parent's required array. Inside an optional ancestor the whole
|
|
196
|
+
* subtree collapses (except for author-forced values: const, default,
|
|
197
|
+
* single-value enum, or seeded values).
|
|
148
198
|
*/
|
|
149
199
|
function initObject(
|
|
150
200
|
schema: Record<string, unknown>,
|
|
151
201
|
root: Record<string, unknown>,
|
|
152
|
-
seed
|
|
202
|
+
seed: Record<string, unknown> | undefined,
|
|
203
|
+
parentRequired: boolean,
|
|
153
204
|
): Record<string, unknown> {
|
|
154
205
|
const properties = schema.properties as
|
|
155
206
|
| Record<string, Record<string, unknown>>
|
|
156
207
|
| undefined;
|
|
157
208
|
if (!properties) {
|
|
158
|
-
// Object with no defined properties — return seed or empty object
|
|
159
209
|
return seed && typeof seed === "object" ? { ...seed } : {};
|
|
160
210
|
}
|
|
161
211
|
|
|
212
|
+
const requiredSet = new Set<string>(
|
|
213
|
+
Array.isArray(schema.required) ? (schema.required as string[]) : [],
|
|
214
|
+
);
|
|
215
|
+
|
|
162
216
|
const result: Record<string, unknown> = {};
|
|
163
217
|
|
|
164
218
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
165
219
|
const seedValue = seed && typeof seed === "object" ? seed[key] : undefined;
|
|
166
|
-
|
|
220
|
+
const effectiveRequired = parentRequired && requiredSet.has(key);
|
|
221
|
+
const value = initProperty(propSchema, root, seedValue, effectiveRequired);
|
|
222
|
+
if (value !== undefined) {
|
|
223
|
+
result[key] = value;
|
|
224
|
+
}
|
|
167
225
|
}
|
|
168
226
|
|
|
169
227
|
return result;
|
|
@@ -171,19 +229,22 @@ function initObject(
|
|
|
171
229
|
|
|
172
230
|
/**
|
|
173
231
|
* Handle oneOf schemas — pick the first variant and initialize it.
|
|
232
|
+
* `required` propagates: if the outer oneOf is required, the variant is
|
|
233
|
+
* materialized with typed-empty seeding for its required fields; if optional,
|
|
234
|
+
* only author-forced values (const / default / single-enum / seed) survive.
|
|
174
235
|
*/
|
|
175
236
|
function initOneOf(
|
|
176
237
|
schema: Record<string, unknown>,
|
|
177
238
|
root: Record<string, unknown>,
|
|
178
|
-
seed
|
|
239
|
+
seed: unknown,
|
|
240
|
+
required: boolean,
|
|
179
241
|
): unknown {
|
|
180
242
|
const variants = schema.oneOf as Record<string, unknown>[];
|
|
181
|
-
if (!variants || variants.length === 0) return null;
|
|
243
|
+
if (!variants || variants.length === 0) return required ? null : undefined;
|
|
182
244
|
|
|
183
245
|
const first = variants[0];
|
|
184
|
-
if (!first) return null;
|
|
246
|
+
if (!first) return required ? null : undefined;
|
|
185
247
|
|
|
186
|
-
// Pick first variant, resolve its $ref if needed
|
|
187
248
|
const firstVariant = resolveRef(first, root);
|
|
188
|
-
return initProperty(firstVariant, root, seed);
|
|
249
|
+
return initProperty(firstVariant, root, seed, required);
|
|
189
250
|
}
|