@narrative.io/jsonforms-provider-protocols 3.0.0-beta.11 → 3.0.0-beta.13
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.
- package/dist/core/initFormData.d.ts +8 -1
- package/dist/core/initFormData.d.ts.map +1 -1
- package/dist/core/initFormData.js +35 -16
- package/dist/core/initFormData.js.map +1 -1
- package/dist/vue/composables/useProjection.d.ts +1 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -1
- package/dist/vue/composables/useProjection.js +33 -5
- package/dist/vue/composables/useProjection.js.map +1 -1
- package/package.json +1 -1
- package/src/core/initFormData.ts +81 -38
- package/src/vue/composables/useProjection.ts +55 -6
|
@@ -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,12 +39,11 @@ 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
|
if (seed !== void 0 && seed !== null && type !== "object") {
|
|
49
49
|
return seed;
|
|
@@ -57,9 +57,22 @@ function initProperty(property, root, seed) {
|
|
|
57
57
|
if ("default" in resolved) {
|
|
58
58
|
return resolved.default;
|
|
59
59
|
}
|
|
60
|
+
if (Array.isArray(resolved.oneOf)) {
|
|
61
|
+
if (!required) return void 0;
|
|
62
|
+
return initOneOf(resolved, root, seed);
|
|
63
|
+
}
|
|
64
|
+
if (type === "object") {
|
|
65
|
+
const obj = initObject(
|
|
66
|
+
resolved,
|
|
67
|
+
root,
|
|
68
|
+
seed,
|
|
69
|
+
required
|
|
70
|
+
);
|
|
71
|
+
if (!required && Object.keys(obj).length === 0) return void 0;
|
|
72
|
+
return obj;
|
|
73
|
+
}
|
|
74
|
+
if (!required) return void 0;
|
|
60
75
|
switch (type) {
|
|
61
|
-
case "object":
|
|
62
|
-
return initObject(resolved, root, seed);
|
|
63
76
|
case "array":
|
|
64
77
|
return seed !== void 0 && seed !== null ? seed : [];
|
|
65
78
|
case "string":
|
|
@@ -68,20 +81,26 @@ function initProperty(property, root, seed) {
|
|
|
68
81
|
return false;
|
|
69
82
|
case "number":
|
|
70
83
|
case "integer":
|
|
71
|
-
return null;
|
|
72
84
|
default:
|
|
73
85
|
return null;
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
|
-
function initObject(schema, root, seed) {
|
|
88
|
+
function initObject(schema, root, seed, parentRequired) {
|
|
77
89
|
const properties = schema.properties;
|
|
78
90
|
if (!properties) {
|
|
79
91
|
return seed && typeof seed === "object" ? { ...seed } : {};
|
|
80
92
|
}
|
|
93
|
+
const requiredSet = new Set(
|
|
94
|
+
Array.isArray(schema.required) ? schema.required : []
|
|
95
|
+
);
|
|
81
96
|
const result = {};
|
|
82
97
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
83
98
|
const seedValue = seed && typeof seed === "object" ? seed[key] : void 0;
|
|
84
|
-
|
|
99
|
+
const effectiveRequired = parentRequired && requiredSet.has(key);
|
|
100
|
+
const value = initProperty(propSchema, root, seedValue, effectiveRequired);
|
|
101
|
+
if (value !== void 0) {
|
|
102
|
+
result[key] = value;
|
|
103
|
+
}
|
|
85
104
|
}
|
|
86
105
|
return result;
|
|
87
106
|
}
|
|
@@ -91,7 +110,7 @@ function initOneOf(schema, root, seed) {
|
|
|
91
110
|
const first = variants[0];
|
|
92
111
|
if (!first) return null;
|
|
93
112
|
const firstVariant = resolveRef(first, root);
|
|
94
|
-
return initProperty(firstVariant, root, seed);
|
|
113
|
+
return initProperty(firstVariant, root, seed, true);
|
|
95
114
|
}
|
|
96
115
|
export {
|
|
97
116
|
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":"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"}
|
|
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\n // Priority 1: seed wins for non-object types.\n if (seed !== undefined && seed !== null && type !== \"object\") {\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. For required fields, materialize the first variant\n // so the discriminator and shape are present. For optional fields, leave\n // undefined so the user isn't forced into a variant they haven't chosen.\n if (Array.isArray(resolved.oneOf)) {\n if (!required) return undefined;\n return initOneOf(resolved, root, seed);\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 * The variant is treated as required so its own required discriminator\n * and structure seed cleanly.\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 const firstVariant = resolveRef(first, root);\n return initProperty(firstVariant, root, seed, true);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useProjection.d.ts","sourceRoot":"","sources":["../../../src/vue/composables/useProjection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"useProjection.d.ts","sourceRoot":"","sources":["../../../src/vue/composables/useProjection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AAQnE,UAAU,iBAAiB;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,QAAQ,EAAE;QAAE,OAAO,CAAC,EAAE;YAAE,UAAU,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAA;KAAE,CAAC;CACzE;AA8FD,MAAM,WAAW,gBAAgB;IAC/B,sDAAsD;IACtD,aAAa,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,gEAAgE;IAEhE,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAClD,8DAA8D;IAC9D,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9D,mCAAmC;IACnC,aAAa,EAAE,OAAO,CAAC;IACvB,sFAAsF;IACtF,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,qEAAqE;IACrE,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,GAAG,CAAC,iBAAiB,CAAC,EAC/B,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GACnD,gBAAgB,CAyFlB"}
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import { computed, inject } from "vue";
|
|
2
|
-
import { getProjectedValue, getProjectedSchema, setProjectedValue } from "../../core/projection.js";
|
|
3
|
-
function resolveLabel(ctrl, schemaTitle) {
|
|
4
|
-
|
|
2
|
+
import { getProjectedValue, getProjectedSchema, parseProjectionPath, setProjectedValue } from "../../core/projection.js";
|
|
3
|
+
function resolveLabel(ctrl, schemaTitle, required) {
|
|
4
|
+
const base = ctrl.uischema?.options?.label ?? schemaTitle ?? ctrl.label ?? "";
|
|
5
|
+
if (!base) return base;
|
|
6
|
+
return required ? `${base} *` : base;
|
|
7
|
+
}
|
|
8
|
+
function isProjectedFieldRequired(schema, path) {
|
|
9
|
+
const segments = parseProjectionPath(path);
|
|
10
|
+
if (segments.length === 0) return false;
|
|
11
|
+
let current = schema;
|
|
12
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
13
|
+
const seg = segments[i];
|
|
14
|
+
if (typeof seg === "number") {
|
|
15
|
+
current = current?.items;
|
|
16
|
+
} else {
|
|
17
|
+
current = current?.properties?.[seg];
|
|
18
|
+
}
|
|
19
|
+
if (!current) return false;
|
|
20
|
+
}
|
|
21
|
+
const last = segments[segments.length - 1];
|
|
22
|
+
if (typeof last !== "string") return false;
|
|
23
|
+
return Array.isArray(current?.required) && current.required.includes(last);
|
|
5
24
|
}
|
|
6
25
|
function normalizeErrors(errors) {
|
|
7
26
|
if (!errors) return errors;
|
|
@@ -21,7 +40,9 @@ function getErrorPath(error) {
|
|
|
21
40
|
function useProjection(control, handleChange) {
|
|
22
41
|
const projection = control.value.uischema?.options?.projection;
|
|
23
42
|
if (!projection) {
|
|
24
|
-
const label2 = computed(
|
|
43
|
+
const label2 = computed(
|
|
44
|
+
() => resolveLabel(control.value, void 0, control.value.required)
|
|
45
|
+
);
|
|
25
46
|
return {
|
|
26
47
|
projectedData: computed(() => control.value.data),
|
|
27
48
|
projectedSchema: computed(() => control.value.schema),
|
|
@@ -47,8 +68,15 @@ function useProjection(control, handleChange) {
|
|
|
47
68
|
const projectedSchema = computed(
|
|
48
69
|
() => getProjectedSchema(control.value.schema, projection)
|
|
49
70
|
);
|
|
71
|
+
const projectedRequired = computed(
|
|
72
|
+
() => isProjectedFieldRequired(control.value.schema, projection)
|
|
73
|
+
);
|
|
50
74
|
const label = computed(
|
|
51
|
-
() => resolveLabel(
|
|
75
|
+
() => resolveLabel(
|
|
76
|
+
control.value,
|
|
77
|
+
projectedSchema.value?.title,
|
|
78
|
+
projectedRequired.value
|
|
79
|
+
)
|
|
52
80
|
);
|
|
53
81
|
const projectedErrors = computed(() => {
|
|
54
82
|
const baseErrors = normalizeErrors(control.value.errors || "");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useProjection.js","sources":["../../../src/vue/composables/useProjection.ts"],"sourcesContent":["import { computed, inject, type ComputedRef, type Ref } from \"vue\";\nimport {\n getProjectedValue,\n setProjectedValue,\n getProjectedSchema,\n} from \"../../core/projection\";\n\ninterface ProjectionControl {\n data: unknown;\n path: string;\n errors: string;\n label?: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n schema: Record<string, any>;\n uischema: { options?: { projection?: string; [key: string]: unknown } };\n}\n\n// Minimal AJV ErrorObject shape for filtering\ninterface ErrorLike {\n instancePath?: string;\n keyword?: string;\n message?: string;\n params?: { missingProperty?: string };\n}\n\n/**\n * Resolve the display label for a control.\n * Priority: uischema options.label → schemaTitle (projected sub-schema) → control.label.\n */\nfunction resolveLabel(ctrl: ProjectionControl, schemaTitle?: string): string {\n return (\n (ctrl.uischema?.options?.label as string) ?? schemaTitle ?? ctrl.label ?? \"\"\n );\n}\n\n/**\n * Normalize AJV error message fragments into user-friendly text.\n * e.g. \"is a required property\" → \"is required\"\n */\nfunction normalizeErrors(errors: string): string {\n if (!errors) return errors;\n return errors\n .replace(/is a required property/g, \"is required\")\n .replace(/must have required property '[^']*'/g, \"is required\");\n}\n\n/**\n * Prefix each error message line with the field label so that AJV fragments\n * like \"is required\" become \"Name is required\".\n */\nfunction prefixErrors(label: string, errors: string): string {\n if (!label || !errors) return errors;\n return errors\n .split(\"\\n\")\n .map((line) => `${label} ${line}`)\n .join(\"\\n\");\n}\n\n/**\n * Convert an AJV ErrorObject's instancePath to a dot-separated control path.\n * Replicates the logic from @jsonforms/core getControlPath.\n */\nfunction getErrorPath(error: ErrorLike): string {\n let p = (error.instancePath || \"\").replace(/\\//g, \".\").replace(/^\\./, \"\");\n if (error.keyword === \"required\" && error.params?.missingProperty) {\n p = p\n ? p + \".\" + error.params.missingProperty\n : error.params.missingProperty;\n }\n return p;\n}\n\nexport interface ProjectionResult {\n /** The value at the projected path (for rendering) */\n projectedData: ComputedRef<unknown>;\n /** The schema at the projected path (for renderer selection) */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n projectedSchema: ComputedRef<Record<string, any>>;\n /** Wrapped handleChange that writes through the projection */\n handleProjectedChange: (path: string, value: unknown) => void;\n /** Whether projection is active */\n hasProjection: boolean;\n /** Resolved display label (options.label → projected schema title → control.label) */\n projectedLabel: ComputedRef<string>;\n /** Error string combining base-path and projected sub-path errors */\n projectedErrors: ComputedRef<string>;\n}\n\n/**\n * Composable that wraps a JSON Forms control with projection support.\n *\n * When `options.projection` is set on the uischema, this composable:\n * - Reads the projected sub-value from the control data\n * - Resolves the projected sub-schema for renderer type resolution\n * - Wraps handleChange to write back through the projection path (preserving siblings)\n *\n * When no projection is set, it passes through control data/schema/handleChange unchanged.\n */\nexport function useProjection(\n control: Ref<ProjectionControl>,\n handleChange: (path: string, value: unknown) => void,\n): ProjectionResult {\n const projection = control.value.uischema?.options?.projection as\n | string\n | undefined;\n\n if (!projection) {\n const label = computed(() => resolveLabel(control.value));\n return {\n projectedData: computed(() => control.value.data),\n projectedSchema: computed(() => control.value.schema),\n handleProjectedChange: handleChange,\n hasProjection: false,\n projectedLabel: label,\n projectedErrors: computed(() =>\n prefixErrors(\n label.value.replace(/\\*$/, \"\").trim(),\n normalizeErrors(control.value.errors),\n ),\n ),\n };\n }\n\n // Inject JSONForms state to access raw AJV errors for projected sub-paths.\n // control.errors only contains errors at the exact control path (e.g. \"data_rates\"),\n // but projected fields need errors at the full path (e.g. \"data_rates.0.video_rate_usd\").\n const jsonforms = inject<{ core?: { errors?: ErrorLike[] } } | null>(\n \"jsonforms\",\n null,\n );\n\n const fullProjectedPath = control.value.path + \".\" + projection;\n\n const projectedData = computed(() =>\n getProjectedValue(control.value.data, projection),\n );\n\n const projectedSchema = computed(() =>\n getProjectedSchema(control.value.schema, projection),\n );\n\n const label = computed(() =>\n resolveLabel(control.value, projectedSchema.value?.title),\n );\n\n const projectedErrors = computed(() => {\n const baseErrors = normalizeErrors(control.value.errors || \"\");\n\n const rawErrors = jsonforms?.core?.errors ?? [];\n const matching = rawErrors.filter(\n (err) => getErrorPath(err) === fullProjectedPath,\n );\n\n let errStr: string;\n if (matching.length === 0) {\n errStr = baseErrors;\n } else {\n const projMsg = matching\n .map((e) => (e.keyword === \"required\" ? \"is required\" : e.message))\n .filter(Boolean)\n .join(\"\\n\");\n errStr = [baseErrors, projMsg].filter(Boolean).join(\"\\n\");\n }\n\n return prefixErrors(label.value.replace(/\\*$/, \"\").trim(), errStr);\n });\n\n const handleProjectedChange = (path: string, value: unknown) => {\n const fullValue = setProjectedValue(control.value.data, projection, value);\n handleChange(path, fullValue);\n };\n\n return {\n projectedData,\n projectedSchema,\n handleProjectedChange,\n hasProjection: true,\n projectedLabel: label,\n projectedErrors,\n };\n}\n"],"names":["label"],"mappings":";;AA6BA,SAAS,aAAa,MAAyB,aAA8B;AAC3E,SACG,KAAK,UAAU,SAAS,SAAoB,eAAe,KAAK,SAAS;AAE9E;AAMA,SAAS,gBAAgB,QAAwB;AAC/C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,OACJ,QAAQ,2BAA2B,aAAa,EAChD,QAAQ,wCAAwC,aAAa;AAClE;AAMA,SAAS,aAAa,OAAe,QAAwB;AAC3D,MAAI,CAAC,SAAS,CAAC,OAAQ,QAAO;AAC9B,SAAO,OACJ,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,GAAG,KAAK,IAAI,IAAI,EAAE,EAChC,KAAK,IAAI;AACd;AAMA,SAAS,aAAa,OAA0B;AAC9C,MAAI,KAAK,MAAM,gBAAgB,IAAI,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AACxE,MAAI,MAAM,YAAY,cAAc,MAAM,QAAQ,iBAAiB;AACjE,QAAI,IACA,IAAI,MAAM,MAAM,OAAO,kBACvB,MAAM,OAAO;AAAA,EACnB;AACA,SAAO;AACT;AA4BO,SAAS,cACd,SACA,cACkB;AAClB,QAAM,aAAa,QAAQ,MAAM,UAAU,SAAS;AAIpD,MAAI,CAAC,YAAY;AACf,UAAMA,SAAQ,SAAS,MAAM,aAAa,QAAQ,KAAK,CAAC;AACxD,WAAO;AAAA,MACL,eAAe,SAAS,MAAM,QAAQ,MAAM,IAAI;AAAA,MAChD,iBAAiB,SAAS,MAAM,QAAQ,MAAM,MAAM;AAAA,MACpD,uBAAuB;AAAA,MACvB,eAAe;AAAA,MACf,gBAAgBA;AAAAA,MAChB,iBAAiB;AAAA,QAAS,MACxB;AAAA,UACEA,OAAM,MAAM,QAAQ,OAAO,EAAE,EAAE,KAAA;AAAA,UAC/B,gBAAgB,QAAQ,MAAM,MAAM;AAAA,QAAA;AAAA,MACtC;AAAA,IACF;AAAA,EAEJ;AAKA,QAAM,YAAY;AAAA,IAChB;AAAA,IACA;AAAA,EAAA;AAGF,QAAM,oBAAoB,QAAQ,MAAM,OAAO,MAAM;AAErD,QAAM,gBAAgB;AAAA,IAAS,MAC7B,kBAAkB,QAAQ,MAAM,MAAM,UAAU;AAAA,EAAA;AAGlD,QAAM,kBAAkB;AAAA,IAAS,MAC/B,mBAAmB,QAAQ,MAAM,QAAQ,UAAU;AAAA,EAAA;AAGrD,QAAM,QAAQ;AAAA,IAAS,MACrB,aAAa,QAAQ,OAAO,gBAAgB,OAAO,KAAK;AAAA,EAAA;AAG1D,QAAM,kBAAkB,SAAS,MAAM;AACrC,UAAM,aAAa,gBAAgB,QAAQ,MAAM,UAAU,EAAE;AAE7D,UAAM,YAAY,WAAW,MAAM,UAAU,CAAA;AAC7C,UAAM,WAAW,UAAU;AAAA,MACzB,CAAC,QAAQ,aAAa,GAAG,MAAM;AAAA,IAAA;AAGjC,QAAI;AACJ,QAAI,SAAS,WAAW,GAAG;AACzB,eAAS;AAAA,IACX,OAAO;AACL,YAAM,UAAU,SACb,IAAI,CAAC,MAAO,EAAE,YAAY,aAAa,gBAAgB,EAAE,OAAQ,EACjE,OAAO,OAAO,EACd,KAAK,IAAI;AACZ,eAAS,CAAC,YAAY,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAAA,IAC1D;AAEA,WAAO,aAAa,MAAM,MAAM,QAAQ,OAAO,EAAE,EAAE,KAAA,GAAQ,MAAM;AAAA,EACnE,CAAC;AAED,QAAM,wBAAwB,CAAC,MAAc,UAAmB;AAC9D,UAAM,YAAY,kBAAkB,QAAQ,MAAM,MAAM,YAAY,KAAK;AACzE,iBAAa,MAAM,SAAS;AAAA,EAC9B;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,gBAAgB;AAAA,IAChB;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"useProjection.js","sources":["../../../src/vue/composables/useProjection.ts"],"sourcesContent":["import { computed, inject, type ComputedRef, type Ref } from \"vue\";\nimport {\n getProjectedValue,\n setProjectedValue,\n getProjectedSchema,\n parseProjectionPath,\n} from \"../../core/projection\";\n\ninterface ProjectionControl {\n data: unknown;\n path: string;\n errors: string;\n label?: string;\n required?: boolean;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n schema: Record<string, any>;\n uischema: { options?: { projection?: string; [key: string]: unknown } };\n}\n\n// Minimal AJV ErrorObject shape for filtering\ninterface ErrorLike {\n instancePath?: string;\n keyword?: string;\n message?: string;\n params?: { missingProperty?: string };\n}\n\n/**\n * Resolve the display label for a control.\n * Priority: uischema options.label → schemaTitle (projected sub-schema) → control.label.\n * Appends a trailing asterisk when the control is for a required property.\n */\nfunction resolveLabel(\n ctrl: ProjectionControl,\n schemaTitle?: string,\n required?: boolean,\n): string {\n const base =\n (ctrl.uischema?.options?.label as string) ??\n schemaTitle ??\n ctrl.label ??\n \"\";\n if (!base) return base;\n return required ? `${base} *` : base;\n}\n\n/**\n * Determine whether the leaf of a projection path is listed in its parent\n * schema's `required` array. Numeric leaf segments (array indices) are not\n * considered \"required properties\".\n */\nfunction isProjectedFieldRequired(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n schema: Record<string, any>,\n path: string,\n): boolean {\n const segments = parseProjectionPath(path);\n if (segments.length === 0) return false;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let current: Record<string, any> = schema;\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n if (typeof seg === \"number\") {\n current = current?.items;\n } else {\n current = current?.properties?.[seg];\n }\n if (!current) return false;\n }\n const last = segments[segments.length - 1];\n if (typeof last !== \"string\") return false;\n return Array.isArray(current?.required) && current.required.includes(last);\n}\n\n/**\n * Normalize AJV error message fragments into user-friendly text.\n * e.g. \"is a required property\" → \"is required\"\n */\nfunction normalizeErrors(errors: string): string {\n if (!errors) return errors;\n return errors\n .replace(/is a required property/g, \"is required\")\n .replace(/must have required property '[^']*'/g, \"is required\");\n}\n\n/**\n * Prefix each error message line with the field label so that AJV fragments\n * like \"is required\" become \"Name is required\".\n */\nfunction prefixErrors(label: string, errors: string): string {\n if (!label || !errors) return errors;\n return errors\n .split(\"\\n\")\n .map((line) => `${label} ${line}`)\n .join(\"\\n\");\n}\n\n/**\n * Convert an AJV ErrorObject's instancePath to a dot-separated control path.\n * Replicates the logic from @jsonforms/core getControlPath.\n */\nfunction getErrorPath(error: ErrorLike): string {\n let p = (error.instancePath || \"\").replace(/\\//g, \".\").replace(/^\\./, \"\");\n if (error.keyword === \"required\" && error.params?.missingProperty) {\n p = p\n ? p + \".\" + error.params.missingProperty\n : error.params.missingProperty;\n }\n return p;\n}\n\nexport interface ProjectionResult {\n /** The value at the projected path (for rendering) */\n projectedData: ComputedRef<unknown>;\n /** The schema at the projected path (for renderer selection) */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n projectedSchema: ComputedRef<Record<string, any>>;\n /** Wrapped handleChange that writes through the projection */\n handleProjectedChange: (path: string, value: unknown) => void;\n /** Whether projection is active */\n hasProjection: boolean;\n /** Resolved display label (options.label → projected schema title → control.label) */\n projectedLabel: ComputedRef<string>;\n /** Error string combining base-path and projected sub-path errors */\n projectedErrors: ComputedRef<string>;\n}\n\n/**\n * Composable that wraps a JSON Forms control with projection support.\n *\n * When `options.projection` is set on the uischema, this composable:\n * - Reads the projected sub-value from the control data\n * - Resolves the projected sub-schema for renderer type resolution\n * - Wraps handleChange to write back through the projection path (preserving siblings)\n *\n * When no projection is set, it passes through control data/schema/handleChange unchanged.\n */\nexport function useProjection(\n control: Ref<ProjectionControl>,\n handleChange: (path: string, value: unknown) => void,\n): ProjectionResult {\n const projection = control.value.uischema?.options?.projection as\n | string\n | undefined;\n\n if (!projection) {\n const label = computed(() =>\n resolveLabel(control.value, undefined, control.value.required),\n );\n return {\n projectedData: computed(() => control.value.data),\n projectedSchema: computed(() => control.value.schema),\n handleProjectedChange: handleChange,\n hasProjection: false,\n projectedLabel: label,\n projectedErrors: computed(() =>\n prefixErrors(\n label.value.replace(/\\*$/, \"\").trim(),\n normalizeErrors(control.value.errors),\n ),\n ),\n };\n }\n\n // Inject JSONForms state to access raw AJV errors for projected sub-paths.\n // control.errors only contains errors at the exact control path (e.g. \"data_rates\"),\n // but projected fields need errors at the full path (e.g. \"data_rates.0.video_rate_usd\").\n const jsonforms = inject<{ core?: { errors?: ErrorLike[] } } | null>(\n \"jsonforms\",\n null,\n );\n\n const fullProjectedPath = control.value.path + \".\" + projection;\n\n const projectedData = computed(() =>\n getProjectedValue(control.value.data, projection),\n );\n\n const projectedSchema = computed(() =>\n getProjectedSchema(control.value.schema, projection),\n );\n\n const projectedRequired = computed(() =>\n isProjectedFieldRequired(control.value.schema, projection),\n );\n\n const label = computed(() =>\n resolveLabel(\n control.value,\n projectedSchema.value?.title,\n projectedRequired.value,\n ),\n );\n\n const projectedErrors = computed(() => {\n const baseErrors = normalizeErrors(control.value.errors || \"\");\n\n const rawErrors = jsonforms?.core?.errors ?? [];\n const matching = rawErrors.filter(\n (err) => getErrorPath(err) === fullProjectedPath,\n );\n\n let errStr: string;\n if (matching.length === 0) {\n errStr = baseErrors;\n } else {\n const projMsg = matching\n .map((e) => (e.keyword === \"required\" ? \"is required\" : e.message))\n .filter(Boolean)\n .join(\"\\n\");\n errStr = [baseErrors, projMsg].filter(Boolean).join(\"\\n\");\n }\n\n return prefixErrors(label.value.replace(/\\*$/, \"\").trim(), errStr);\n });\n\n const handleProjectedChange = (path: string, value: unknown) => {\n const fullValue = setProjectedValue(control.value.data, projection, value);\n handleChange(path, fullValue);\n };\n\n return {\n projectedData,\n projectedSchema,\n handleProjectedChange,\n hasProjection: true,\n projectedLabel: label,\n projectedErrors,\n };\n}\n"],"names":["label"],"mappings":"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"}
|
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,33 @@ 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;
|
|
108
|
+
|
|
109
|
+
// Priority 1: seed wins for non-object types.
|
|
106
110
|
if (seed !== undefined && seed !== null && type !== "object") {
|
|
107
111
|
return seed;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
// Priority 2: const
|
|
114
|
+
// Priority 2: const (schema-invariant — always set).
|
|
111
115
|
if ("const" in resolved) {
|
|
112
116
|
return resolved.const;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
// Priority 3: single-value enum
|
|
119
|
+
// Priority 3: single-value enum (same reasoning — only one valid value).
|
|
116
120
|
if (
|
|
117
121
|
Array.isArray(resolved.enum) &&
|
|
118
122
|
(resolved.enum as unknown[]).length === 1
|
|
@@ -120,15 +124,39 @@ function initProperty(
|
|
|
120
124
|
return (resolved.enum as unknown[])[0];
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
// Priority 4: default
|
|
127
|
+
// Priority 4: default (explicit author intent — always honored).
|
|
124
128
|
if ("default" in resolved) {
|
|
125
129
|
return resolved.default;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
// Priority 5:
|
|
132
|
+
// Priority 5: oneOf. For required fields, materialize the first variant
|
|
133
|
+
// so the discriminator and shape are present. For optional fields, leave
|
|
134
|
+
// undefined so the user isn't forced into a variant they haven't chosen.
|
|
135
|
+
if (Array.isArray(resolved.oneOf)) {
|
|
136
|
+
if (!required) return undefined;
|
|
137
|
+
return initOneOf(resolved, root, seed);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Priority 6: objects recurse. The `required` flag propagates downward:
|
|
141
|
+
// inside an optional ancestor, nested required primitives also collapse,
|
|
142
|
+
// so an optional object with nested required fields stays absent instead
|
|
143
|
+
// of materializing a shell that AJV would flag.
|
|
144
|
+
if (type === "object") {
|
|
145
|
+
const obj = initObject(
|
|
146
|
+
resolved,
|
|
147
|
+
root,
|
|
148
|
+
seed as Record<string, unknown>,
|
|
149
|
+
required,
|
|
150
|
+
);
|
|
151
|
+
if (!required && Object.keys(obj).length === 0) return undefined;
|
|
152
|
+
return obj;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Priority 7: optional primitives — omit.
|
|
156
|
+
if (!required) return undefined;
|
|
157
|
+
|
|
158
|
+
// Priority 8: required primitives — legacy typed empty.
|
|
129
159
|
switch (type) {
|
|
130
|
-
case "object":
|
|
131
|
-
return initObject(resolved, root, seed as Record<string, unknown>);
|
|
132
160
|
case "array":
|
|
133
161
|
return seed !== undefined && seed !== null ? seed : [];
|
|
134
162
|
case "string":
|
|
@@ -137,7 +165,6 @@ function initProperty(
|
|
|
137
165
|
return false;
|
|
138
166
|
case "number":
|
|
139
167
|
case "integer":
|
|
140
|
-
return null;
|
|
141
168
|
default:
|
|
142
169
|
return null;
|
|
143
170
|
}
|
|
@@ -145,25 +172,40 @@ function initProperty(
|
|
|
145
172
|
|
|
146
173
|
/**
|
|
147
174
|
* Initialize an object type by recursing into its properties.
|
|
175
|
+
* Keys whose initialization returns `undefined` are omitted.
|
|
176
|
+
*
|
|
177
|
+
* `parentRequired` controls how schema.required propagates: a child is
|
|
178
|
+
* treated as required only if both its parent is required AND it appears in
|
|
179
|
+
* the parent's required array. Inside an optional ancestor the whole
|
|
180
|
+
* subtree collapses (except for author-forced values: const, default,
|
|
181
|
+
* single-value enum, or seeded values).
|
|
148
182
|
*/
|
|
149
183
|
function initObject(
|
|
150
184
|
schema: Record<string, unknown>,
|
|
151
185
|
root: Record<string, unknown>,
|
|
152
|
-
seed
|
|
186
|
+
seed: Record<string, unknown> | undefined,
|
|
187
|
+
parentRequired: boolean,
|
|
153
188
|
): Record<string, unknown> {
|
|
154
189
|
const properties = schema.properties as
|
|
155
190
|
| Record<string, Record<string, unknown>>
|
|
156
191
|
| undefined;
|
|
157
192
|
if (!properties) {
|
|
158
|
-
// Object with no defined properties — return seed or empty object
|
|
159
193
|
return seed && typeof seed === "object" ? { ...seed } : {};
|
|
160
194
|
}
|
|
161
195
|
|
|
196
|
+
const requiredSet = new Set<string>(
|
|
197
|
+
Array.isArray(schema.required) ? (schema.required as string[]) : [],
|
|
198
|
+
);
|
|
199
|
+
|
|
162
200
|
const result: Record<string, unknown> = {};
|
|
163
201
|
|
|
164
202
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
165
203
|
const seedValue = seed && typeof seed === "object" ? seed[key] : undefined;
|
|
166
|
-
|
|
204
|
+
const effectiveRequired = parentRequired && requiredSet.has(key);
|
|
205
|
+
const value = initProperty(propSchema, root, seedValue, effectiveRequired);
|
|
206
|
+
if (value !== undefined) {
|
|
207
|
+
result[key] = value;
|
|
208
|
+
}
|
|
167
209
|
}
|
|
168
210
|
|
|
169
211
|
return result;
|
|
@@ -171,6 +213,8 @@ function initObject(
|
|
|
171
213
|
|
|
172
214
|
/**
|
|
173
215
|
* Handle oneOf schemas — pick the first variant and initialize it.
|
|
216
|
+
* The variant is treated as required so its own required discriminator
|
|
217
|
+
* and structure seed cleanly.
|
|
174
218
|
*/
|
|
175
219
|
function initOneOf(
|
|
176
220
|
schema: Record<string, unknown>,
|
|
@@ -183,7 +227,6 @@ function initOneOf(
|
|
|
183
227
|
const first = variants[0];
|
|
184
228
|
if (!first) return null;
|
|
185
229
|
|
|
186
|
-
// Pick first variant, resolve its $ref if needed
|
|
187
230
|
const firstVariant = resolveRef(first, root);
|
|
188
|
-
return initProperty(firstVariant, root, seed);
|
|
231
|
+
return initProperty(firstVariant, root, seed, true);
|
|
189
232
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
getProjectedValue,
|
|
4
4
|
setProjectedValue,
|
|
5
5
|
getProjectedSchema,
|
|
6
|
+
parseProjectionPath,
|
|
6
7
|
} from "../../core/projection";
|
|
7
8
|
|
|
8
9
|
interface ProjectionControl {
|
|
@@ -10,6 +11,7 @@ interface ProjectionControl {
|
|
|
10
11
|
path: string;
|
|
11
12
|
errors: string;
|
|
12
13
|
label?: string;
|
|
14
|
+
required?: boolean;
|
|
13
15
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
16
|
schema: Record<string, any>;
|
|
15
17
|
uischema: { options?: { projection?: string; [key: string]: unknown } };
|
|
@@ -26,11 +28,48 @@ interface ErrorLike {
|
|
|
26
28
|
/**
|
|
27
29
|
* Resolve the display label for a control.
|
|
28
30
|
* Priority: uischema options.label → schemaTitle (projected sub-schema) → control.label.
|
|
31
|
+
* Appends a trailing asterisk when the control is for a required property.
|
|
29
32
|
*/
|
|
30
|
-
function resolveLabel(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
function resolveLabel(
|
|
34
|
+
ctrl: ProjectionControl,
|
|
35
|
+
schemaTitle?: string,
|
|
36
|
+
required?: boolean,
|
|
37
|
+
): string {
|
|
38
|
+
const base =
|
|
39
|
+
(ctrl.uischema?.options?.label as string) ??
|
|
40
|
+
schemaTitle ??
|
|
41
|
+
ctrl.label ??
|
|
42
|
+
"";
|
|
43
|
+
if (!base) return base;
|
|
44
|
+
return required ? `${base} *` : base;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Determine whether the leaf of a projection path is listed in its parent
|
|
49
|
+
* schema's `required` array. Numeric leaf segments (array indices) are not
|
|
50
|
+
* considered "required properties".
|
|
51
|
+
*/
|
|
52
|
+
function isProjectedFieldRequired(
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
schema: Record<string, any>,
|
|
55
|
+
path: string,
|
|
56
|
+
): boolean {
|
|
57
|
+
const segments = parseProjectionPath(path);
|
|
58
|
+
if (segments.length === 0) return false;
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
let current: Record<string, any> = schema;
|
|
61
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
62
|
+
const seg = segments[i]!;
|
|
63
|
+
if (typeof seg === "number") {
|
|
64
|
+
current = current?.items;
|
|
65
|
+
} else {
|
|
66
|
+
current = current?.properties?.[seg];
|
|
67
|
+
}
|
|
68
|
+
if (!current) return false;
|
|
69
|
+
}
|
|
70
|
+
const last = segments[segments.length - 1];
|
|
71
|
+
if (typeof last !== "string") return false;
|
|
72
|
+
return Array.isArray(current?.required) && current.required.includes(last);
|
|
34
73
|
}
|
|
35
74
|
|
|
36
75
|
/**
|
|
@@ -105,7 +144,9 @@ export function useProjection(
|
|
|
105
144
|
| undefined;
|
|
106
145
|
|
|
107
146
|
if (!projection) {
|
|
108
|
-
const label = computed(() =>
|
|
147
|
+
const label = computed(() =>
|
|
148
|
+
resolveLabel(control.value, undefined, control.value.required),
|
|
149
|
+
);
|
|
109
150
|
return {
|
|
110
151
|
projectedData: computed(() => control.value.data),
|
|
111
152
|
projectedSchema: computed(() => control.value.schema),
|
|
@@ -139,8 +180,16 @@ export function useProjection(
|
|
|
139
180
|
getProjectedSchema(control.value.schema, projection),
|
|
140
181
|
);
|
|
141
182
|
|
|
183
|
+
const projectedRequired = computed(() =>
|
|
184
|
+
isProjectedFieldRequired(control.value.schema, projection),
|
|
185
|
+
);
|
|
186
|
+
|
|
142
187
|
const label = computed(() =>
|
|
143
|
-
resolveLabel(
|
|
188
|
+
resolveLabel(
|
|
189
|
+
control.value,
|
|
190
|
+
projectedSchema.value?.title,
|
|
191
|
+
projectedRequired.value,
|
|
192
|
+
),
|
|
144
193
|
);
|
|
145
194
|
|
|
146
195
|
const projectedErrors = computed(() => {
|