@narrative.io/jsonforms-provider-protocols 2.11.0-beta.0 → 2.12.0

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.
Files changed (148) hide show
  1. package/README.md +193 -33
  2. package/dist/core/initFormData.d.ts +17 -0
  3. package/dist/core/initFormData.d.ts.map +1 -0
  4. package/dist/core/initFormData.js +99 -0
  5. package/dist/core/initFormData.js.map +1 -0
  6. package/dist/core/projection.d.ts +36 -0
  7. package/dist/core/projection.d.ts.map +1 -0
  8. package/dist/core/projection.js +77 -0
  9. package/dist/core/projection.js.map +1 -0
  10. package/dist/core/refs.d.ts +58 -0
  11. package/dist/core/refs.d.ts.map +1 -0
  12. package/dist/core/refs.js +70 -0
  13. package/dist/core/refs.js.map +1 -0
  14. package/dist/core/resolveScope.d.ts +17 -0
  15. package/dist/core/resolveScope.d.ts.map +1 -0
  16. package/dist/core/resolveScope.js +28 -0
  17. package/dist/core/resolveScope.js.map +1 -0
  18. package/dist/core/seedProjectionTargets.d.ts +60 -0
  19. package/dist/core/seedProjectionTargets.d.ts.map +1 -0
  20. package/dist/core/seedProjectionTargets.js +52 -0
  21. package/dist/core/seedProjectionTargets.js.map +1 -0
  22. package/dist/core/transforms.d.ts +8 -10
  23. package/dist/core/transforms.d.ts.map +1 -1
  24. package/dist/core/transforms.js +58 -13
  25. package/dist/core/transforms.js.map +1 -1
  26. package/dist/core/types.d.ts +8 -0
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/index.d.ts +9 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +21 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/jsonforms-provider-protocols.css +6 -2
  33. package/dist/no-eval-ajv.d.ts +70 -0
  34. package/dist/no-eval-ajv.d.ts.map +1 -0
  35. package/dist/no-eval-ajv.js +247 -0
  36. package/dist/no-eval-ajv.js.map +1 -0
  37. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  38. package/dist/vue/components/ProviderAutocomplete.vue.js +10 -4
  39. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  40. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  41. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  42. package/dist/vue/components/ProviderMultiSelect.vue2.js +19 -9
  43. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  44. package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts +9 -0
  45. package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts.map +1 -0
  46. package/dist/vue/components/ProviderObjectMultiSelect.vue.js +8 -0
  47. package/dist/vue/components/ProviderObjectMultiSelect.vue.js.map +1 -0
  48. package/dist/vue/components/ProviderObjectMultiSelect.vue2.js +142 -0
  49. package/dist/vue/components/ProviderObjectMultiSelect.vue2.js.map +1 -0
  50. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  51. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  52. package/dist/vue/components/ProviderSelect.vue2.js +20 -8
  53. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  54. package/dist/vue/composables/useDataLayer.d.ts +10 -0
  55. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  56. package/dist/vue/composables/useDataLayer.js +26 -0
  57. package/dist/vue/composables/useDataLayer.js.map +1 -0
  58. package/dist/vue/composables/useDerive.d.ts +5 -2
  59. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  60. package/dist/vue/composables/useDerive.js +29 -12
  61. package/dist/vue/composables/useDerive.js.map +1 -1
  62. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  63. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  64. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  65. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  66. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  67. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  68. package/dist/vue/composables/useDirtyValidation.js +15 -0
  69. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  70. package/dist/vue/composables/useProjection.d.ts +42 -0
  71. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  72. package/dist/vue/composables/useProjection.js +116 -0
  73. package/dist/vue/composables/useProjection.js.map +1 -0
  74. package/dist/vue/composables/useProvider.d.ts +2 -2
  75. package/dist/vue/composables/useProvider.d.ts.map +1 -1
  76. package/dist/vue/composables/useProvider.js +14 -10
  77. package/dist/vue/composables/useProvider.js.map +1 -1
  78. package/dist/vue/index.d.ts +9 -1
  79. package/dist/vue/index.d.ts.map +1 -1
  80. package/dist/vue/index.js +72 -34
  81. package/dist/vue/index.js.map +1 -1
  82. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  83. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  84. package/dist/vue/primevue/JfBoolean.vue.js +42 -21
  85. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  86. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  87. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  88. package/dist/vue/primevue/JfEnum.vue.js +35 -21
  89. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  90. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  91. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  92. package/dist/vue/primevue/JfEnumArray.vue.js +37 -17
  93. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  94. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  95. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  96. package/dist/vue/primevue/JfNumber.vue.js +30 -20
  97. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  98. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  99. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  100. package/dist/vue/primevue/JfText.vue.js +48 -32
  101. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  102. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  103. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  104. package/dist/vue/primevue/JfTextArea.vue.js +31 -16
  105. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  106. package/dist/vue/primevue/index.d.ts.map +1 -1
  107. package/dist/vue/primevue/index.js +74 -7
  108. package/dist/vue/primevue/index.js.map +1 -1
  109. package/dist/vue/utils/autoSelect.js.map +1 -1
  110. package/dist/vue/utils/objectMultiSelect.d.ts +68 -0
  111. package/dist/vue/utils/objectMultiSelect.d.ts.map +1 -0
  112. package/dist/vue/utils/objectMultiSelect.js +72 -0
  113. package/dist/vue/utils/objectMultiSelect.js.map +1 -0
  114. package/dist/vue/utils/placeholder.d.ts +17 -0
  115. package/dist/vue/utils/placeholder.d.ts.map +1 -0
  116. package/dist/vue/utils/placeholder.js +17 -0
  117. package/dist/vue/utils/placeholder.js.map +1 -0
  118. package/package.json +10 -2
  119. package/src/core/initFormData.ts +208 -0
  120. package/src/core/projection.ts +147 -0
  121. package/src/core/refs.ts +166 -0
  122. package/src/core/resolveScope.ts +54 -0
  123. package/src/core/seedProjectionTargets.ts +144 -0
  124. package/src/core/transforms.ts +118 -26
  125. package/src/core/types.ts +9 -0
  126. package/src/index.ts +22 -2
  127. package/src/no-eval-ajv.ts +381 -0
  128. package/src/vue/components/ProviderAutocomplete.vue +10 -6
  129. package/src/vue/components/ProviderMultiSelect.vue +22 -15
  130. package/src/vue/components/ProviderObjectMultiSelect.vue +169 -0
  131. package/src/vue/components/ProviderSelect.vue +23 -14
  132. package/src/vue/composables/useDataLayer.ts +43 -0
  133. package/src/vue/composables/useDerive.ts +62 -16
  134. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  135. package/src/vue/composables/useDirtyValidation.ts +20 -0
  136. package/src/vue/composables/useProjection.ts +245 -0
  137. package/src/vue/composables/useProvider.ts +28 -11
  138. package/src/vue/index.ts +83 -47
  139. package/src/vue/primevue/JfBoolean.vue +35 -12
  140. package/src/vue/primevue/JfEnum.vue +34 -25
  141. package/src/vue/primevue/JfEnumArray.vue +36 -19
  142. package/src/vue/primevue/JfNumber.vue +30 -22
  143. package/src/vue/primevue/JfText.vue +46 -31
  144. package/src/vue/primevue/JfTextArea.vue +30 -19
  145. package/src/vue/primevue/index.ts +88 -7
  146. package/src/vue/utils/autoSelect.ts +2 -2
  147. package/src/vue/utils/objectMultiSelect.ts +171 -0
  148. package/src/vue/utils/placeholder.ts +42 -0
@@ -0,0 +1,147 @@
1
+ import { deref as derefSchema, tryCombinatorBranches } from "./refs";
2
+
3
+ /**
4
+ * Projection utilities for navigating complex data structures
5
+ * through a dot-separated path where numeric segments are array indices.
6
+ *
7
+ * Examples:
8
+ * "0" → first element of an array
9
+ * "include" → the `include` property of an object
10
+ * "0.video_rate_usd" → nested property inside the first array element
11
+ */
12
+
13
+ export type ProjectionSegment = string | number;
14
+
15
+ /**
16
+ * Parse a projection path string into typed segments.
17
+ * Numeric strings become numbers (array indices), others stay as strings (object keys).
18
+ */
19
+ export function parseProjectionPath(path: string): ProjectionSegment[] {
20
+ if (!path) return [];
21
+ return path.split(".").map((s) => {
22
+ const n = Number(s);
23
+ return Number.isInteger(n) && n >= 0 ? n : s;
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Read a value from `data` by following the projection path.
29
+ * Returns `undefined` if any segment along the path is missing.
30
+ */
31
+ export function getProjectedValue(data: unknown, path: string): unknown {
32
+ const segments = parseProjectionPath(path);
33
+ let current: unknown = data;
34
+
35
+ for (const seg of segments) {
36
+ if (current === null || current === undefined) return undefined;
37
+
38
+ if (typeof seg === "number") {
39
+ if (!Array.isArray(current)) return undefined;
40
+ current = current[seg];
41
+ } else {
42
+ if (typeof current !== "object") return undefined;
43
+ current = (current as Record<string, unknown>)[seg];
44
+ }
45
+ }
46
+
47
+ return current;
48
+ }
49
+
50
+ /**
51
+ * Immutably set a value at the projection path, preserving all sibling data.
52
+ * Constructs missing intermediate structures (arrays for numeric segments, objects for string segments).
53
+ */
54
+ export function setProjectedValue(
55
+ data: unknown,
56
+ path: string,
57
+ value: unknown,
58
+ ): unknown {
59
+ const segments = parseProjectionPath(path);
60
+ return setAtPath(data, segments, 0, value);
61
+ }
62
+
63
+ function setAtPath(
64
+ current: unknown,
65
+ segments: ProjectionSegment[],
66
+ index: number,
67
+ value: unknown,
68
+ ): unknown {
69
+ if (index === segments.length) {
70
+ return value;
71
+ }
72
+
73
+ const seg = segments[index]!;
74
+
75
+ if (typeof seg === "number") {
76
+ // Array index — ensure we have an array
77
+ const arr = Array.isArray(current) ? [...current] : [];
78
+ // Pad array if index is out of bounds
79
+ while (arr.length <= seg) {
80
+ arr.push(undefined);
81
+ }
82
+ arr[seg] = setAtPath(arr[seg], segments, index + 1, value);
83
+ return arr;
84
+ } else {
85
+ // Object key — ensure we have an object
86
+ const obj: Record<string, unknown> =
87
+ current !== null &&
88
+ current !== undefined &&
89
+ typeof current === "object" &&
90
+ !Array.isArray(current)
91
+ ? { ...(current as Record<string, unknown>) }
92
+ : {};
93
+ obj[seg] = setAtPath(obj[seg], segments, index + 1, value);
94
+ return obj;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Resolve the schema at the projected path.
100
+ * Numeric segments traverse into `items` (array item schema).
101
+ * String segments traverse into `properties[segment]`.
102
+ *
103
+ * Dereferences `$ref` nodes transparently at every step, and falls through
104
+ * to `oneOf` / `anyOf` / `allOf` branches when a segment can't resolve
105
+ * directly — picks the first branch that satisfies the navigation.
106
+ */
107
+ export function getProjectedSchema(
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ schema: Record<string, any>,
110
+ path: string,
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ ): Record<string, any> {
113
+ const segments = parseProjectionPath(path);
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+ let current: Record<string, any> | undefined = schema;
116
+
117
+ for (const seg of segments) {
118
+ current = derefSchema(current, schema);
119
+ if (!current) return {};
120
+
121
+ const navigate = (
122
+ node: Record<string, unknown>,
123
+ ): Record<string, unknown> | undefined => {
124
+ if (typeof seg === "number") {
125
+ const items = (node as { items?: unknown }).items;
126
+ return items && typeof items === "object"
127
+ ? (items as Record<string, unknown>)
128
+ : undefined;
129
+ }
130
+ const properties = (node as { properties?: unknown }).properties as
131
+ | Record<string, Record<string, unknown>>
132
+ | undefined;
133
+ if (properties && properties[seg]) return properties[seg];
134
+ return undefined;
135
+ };
136
+
137
+ let next = navigate(current);
138
+ if (next === undefined) {
139
+ next = tryCombinatorBranches(current, schema, navigate);
140
+ }
141
+ if (!next) return {};
142
+ current = next;
143
+ }
144
+
145
+ const resolved = derefSchema(current, schema);
146
+ return resolved ?? {};
147
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * JSON Schema $ref resolution helpers.
3
+ *
4
+ * Supports the pointer grammar used across consumer schemas:
5
+ * - "#/$defs/Name"
6
+ * - "#/properties/foo/items"
7
+ *
8
+ * External refs (URIs, file refs) are intentionally out of scope. A ref that
9
+ * doesn't resolve leaves the node untouched — callers can still inspect the
10
+ * unresolved `$ref` for debugging.
11
+ */
12
+
13
+ /**
14
+ * Resolve a JSON pointer (`#/a/b/c`) against an object. Returns the node at
15
+ * that path, or `undefined` if any segment is missing or the pointer doesn't
16
+ * start with `#/`.
17
+ */
18
+ export function resolvePointer(
19
+ obj: Record<string, unknown>,
20
+ pointer: string,
21
+ ): unknown {
22
+ if (!pointer.startsWith("#/")) return undefined;
23
+ const parts = pointer.slice(2).split("/");
24
+ let current: unknown = obj;
25
+ for (const part of parts) {
26
+ if (current && typeof current === "object" && part in current) {
27
+ current = (current as Record<string, unknown>)[part];
28
+ } else {
29
+ return undefined;
30
+ }
31
+ }
32
+ return current;
33
+ }
34
+
35
+ /**
36
+ * Dereference a schema node along a chain of `$ref`s. Follows `A → B → C`
37
+ * transitively. A cycle (same `$ref` seen twice in one chain) returns the
38
+ * last unresolved node rather than hanging. An unresolvable pointer returns
39
+ * the current node unchanged.
40
+ */
41
+ export function resolveRef(
42
+ property: Record<string, unknown>,
43
+ root: Record<string, unknown>,
44
+ seen?: Set<string>,
45
+ ): Record<string, unknown> {
46
+ if (!property || typeof property !== "object") return property;
47
+
48
+ const ref = property.$ref as string | undefined;
49
+ if (!ref) return property;
50
+
51
+ const visited = seen ?? new Set<string>();
52
+ if (visited.has(ref)) return property;
53
+ visited.add(ref);
54
+
55
+ const resolved = resolvePointer(root, ref);
56
+ if (!resolved) return property;
57
+
58
+ return resolveRef(resolved as Record<string, unknown>, root, visited);
59
+ }
60
+
61
+ /**
62
+ * Convenience wrapper around `resolveRef` that starts a fresh cycle-detection
63
+ * set. Intended for schema walkers that need to dereference at every step;
64
+ * each call is an independent resolution.
65
+ */
66
+ export function deref(
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ node: Record<string, any> | undefined,
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ root: Record<string, any>,
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ ): Record<string, any> | undefined {
73
+ if (!node || typeof node !== "object") return node;
74
+ return resolveRef(node, root) as Record<string, unknown> | undefined;
75
+ }
76
+
77
+ /**
78
+ * Depth limit for combinator (oneOf/anyOf/allOf) branch descent. Schemas
79
+ * rarely nest combinators beyond one or two levels; this guard protects
80
+ * against pathological nesting and cycles (e.g. `oneOf: [$ref back to self]`).
81
+ */
82
+ const COMBINATOR_DEPTH_LIMIT = 8;
83
+
84
+ /**
85
+ * Try to navigate a segment through a schema node's combinator branches
86
+ * (`oneOf` / `anyOf` / `allOf`) when direct navigation has failed.
87
+ *
88
+ * Semantics: for walker purposes (renderer-tester matching), we only need
89
+ * ONE concrete schema that satisfies the next navigation step. First-match
90
+ * by structural shape wins, same convention as `initOneOf` uses for seeding.
91
+ *
92
+ * @param node the schema node to search (already dereffed by caller)
93
+ * @param root the root schema, for dereferencing branch `$ref`s
94
+ * @param tryFn predicate that attempts navigation on a candidate branch
95
+ * and returns the navigated value, or `undefined` if the
96
+ * branch doesn't have what the caller's looking for
97
+ * @param depth recursion depth (capped at `COMBINATOR_DEPTH_LIMIT`)
98
+ */
99
+ export function tryCombinatorBranches<T>(
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ node: Record<string, any> | undefined,
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ root: Record<string, any>,
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ tryFn: (candidate: Record<string, any>) => T | undefined,
106
+ depth = 0,
107
+ ): T | undefined {
108
+ if (depth > COMBINATOR_DEPTH_LIMIT) return undefined;
109
+ if (!node || typeof node !== "object") return undefined;
110
+
111
+ const branches = (node.oneOf || node.anyOf || node.allOf) as
112
+ | Record<string, unknown>[]
113
+ | undefined;
114
+ if (!Array.isArray(branches)) return undefined;
115
+
116
+ for (const raw of branches) {
117
+ const branch = deref(raw as Record<string, unknown>, root);
118
+ if (!branch || typeof branch !== "object") continue;
119
+
120
+ const direct = tryFn(branch);
121
+ if (direct !== undefined) return direct;
122
+
123
+ const nested = tryCombinatorBranches(branch, root, tryFn, depth + 1);
124
+ if (nested !== undefined) return nested;
125
+ }
126
+ return undefined;
127
+ }
128
+
129
+ /**
130
+ * Recursively dereference every `$ref` in a schema subtree, producing a
131
+ * concrete schema with no remaining refs. Cycles along any single chain are
132
+ * handled by leaving the first recursion back into a seen ref as the
133
+ * unresolved node — matching `resolveRef`'s semantics.
134
+ *
135
+ * Intended for use at API boundaries (e.g. `resolveScopeSchema`'s return)
136
+ * so downstream walkers can operate on self-contained schemas without
137
+ * needing the original root.
138
+ */
139
+ export function deepDeref(
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ node: any,
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ root: Record<string, any>,
144
+ seen: Set<string> = new Set(),
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ ): any {
147
+ if (!node || typeof node !== "object") return node;
148
+ if (Array.isArray(node)) return node.map((n) => deepDeref(n, root, seen));
149
+
150
+ const ref = (node as Record<string, unknown>).$ref as string | undefined;
151
+ if (typeof ref === "string") {
152
+ if (seen.has(ref)) return node;
153
+ const resolved = resolvePointer(root, ref);
154
+ if (!resolved || typeof resolved !== "object") return node;
155
+ const nextSeen = new Set(seen);
156
+ nextSeen.add(ref);
157
+ return deepDeref(resolved, root, nextSeen);
158
+ }
159
+
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ const result: Record<string, any> = {};
162
+ for (const [key, value] of Object.entries(node)) {
163
+ result[key] = deepDeref(value, root, seen);
164
+ }
165
+ return result;
166
+ }
@@ -0,0 +1,54 @@
1
+ import { deepDeref, deref, tryCombinatorBranches } from "./refs";
2
+
3
+ /**
4
+ * Resolve a JSON Forms scope path to its schema within a root schema.
5
+ * Handles nested paths like "#/properties/parent/properties/child".
6
+ *
7
+ * Follows JSON Schema structure:
8
+ * - "properties" segments navigate into object `.properties`
9
+ * - "items" segments navigate into array `.items`
10
+ * - all other segments index directly into the current object
11
+ *
12
+ * Dereferences `$ref` nodes transparently at every step, and falls through
13
+ * to `oneOf` / `anyOf` / `allOf` branches when a segment can't resolve
14
+ * directly — picks the first branch that satisfies the navigation. The
15
+ * returned schema is deep-dereferenced so downstream walkers can operate on
16
+ * a self-contained sub-schema without needing the original root.
17
+ */
18
+ export function resolveScopeSchema(
19
+ scope: string,
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ rootSchema: Record<string, any>,
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ ): Record<string, any> | undefined {
24
+ if (!scope || !rootSchema) return undefined;
25
+
26
+ // Remove the leading "#/" and split into segments
27
+ const path = scope.replace(/^#\/?/, "");
28
+ if (!path) return deepDeref(rootSchema, rootSchema);
29
+
30
+ const segments = path.split("/");
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ let current: any = rootSchema;
33
+
34
+ for (const segment of segments) {
35
+ current = deref(current, rootSchema);
36
+ if (!current || typeof current !== "object") return undefined;
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const navigate = (node: Record<string, any>): unknown => {
40
+ if (segment === "properties") return node.properties;
41
+ if (segment === "items") return node.items;
42
+ return node[segment];
43
+ };
44
+
45
+ let next = navigate(current);
46
+ if (next === undefined) {
47
+ next = tryCombinatorBranches(current, rootSchema, navigate);
48
+ }
49
+ if (next === undefined) return undefined;
50
+ current = next;
51
+ }
52
+
53
+ return deepDeref(current, rootSchema);
54
+ }
@@ -0,0 +1,144 @@
1
+ import {
2
+ getProjectedValue,
3
+ parseProjectionPath,
4
+ setProjectedValue,
5
+ } from "./projection";
6
+
7
+ /**
8
+ * Materialize array indices targeted by `options.projection` controls in a
9
+ * UI schema, so that JSON Schema validators (AJV / cfworker) emit
10
+ * `items.required` errors on otherwise-empty arrays.
11
+ *
12
+ * Why this exists:
13
+ * `initFormDataFromSchema` omits optional fields, so an optional array
14
+ * like `data_rates: { type: 'array', items: { $ref: '#/$defs/DataRate' } }`
15
+ * produces `undefined` (or, historically, `[]`). Both shapes pass schema
16
+ * validation: `undefined` doesn't trigger `type: array`, and `[]` has no
17
+ * items to apply `items.required` to. As a result, a projection-targeted
18
+ * field rendered with `Video CPM rate *` lies — the asterisk says
19
+ * "required" but the validator never enforces it on an untouched form.
20
+ *
21
+ * This utility opts the consumer into per-item enforcement by walking the
22
+ * UI schema, finding every `Control` with `options.projection` that
23
+ * addresses an array index, and ensuring the corresponding data path has
24
+ * that index materialized as at least `{}`. With the empty object in
25
+ * place, the schema's `items.required` fires and the projected control's
26
+ * error string surfaces at form-validity time.
27
+ *
28
+ * Tradeoff:
29
+ * This re-introduces "noise" on untouched forms — required-field errors
30
+ * for fields the user hasn't seen yet. Use it when validation enforcement
31
+ * on projection targets matters more than a clean initial form state.
32
+ * The alternative is to declare these arrays as `required` + `minItems: 1`
33
+ * + `default: [{}]` at the schema level, which avoids needing this helper.
34
+ *
35
+ * Properties:
36
+ * - Idempotent: running twice yields the same result as running once.
37
+ * - Non-destructive: existing values at target paths are preserved.
38
+ * - Pure: returns a new object; does not mutate `data`.
39
+ *
40
+ * Example:
41
+ * ```ts
42
+ * const data = initFormDataFromSchema(schema);
43
+ * const seeded = seedProjectionTargets(data, uischema);
44
+ * // For uischema controls like
45
+ * // { type: 'Control', scope: '#/properties/data_rates',
46
+ * // options: { projection: '0.video_rate_usd' } }
47
+ * // seeded.data_rates is now `[{}]` (was `undefined` or `[]`).
48
+ * ```
49
+ */
50
+ export function seedProjectionTargets(
51
+ data: unknown,
52
+ uischema: UISchemaLike | UISchemaLike[] | undefined,
53
+ ): unknown {
54
+ if (!uischema) return data;
55
+
56
+ const controls: { scope: string; projection: string }[] = [];
57
+ collectProjectionControls(uischema, controls);
58
+
59
+ let result = data;
60
+ for (const { scope, projection } of controls) {
61
+ const dataPath = scopeToDataPath(scope);
62
+ const segments = parseProjectionPath(projection);
63
+
64
+ // Seed only when a numeric segment is followed by a *string* segment —
65
+ // i.e., the consumer is reading a property of the array item, so the
66
+ // item must be an object. Numeric-at-end (`'0'`) addresses the array
67
+ // element itself, which can be any type (primitive, nested array,
68
+ // object) — we can't infer it from the uischema alone, so leave it
69
+ // alone. Numeric-followed-by-numeric (`'0.0'`) is a nested array; we'd
70
+ // need to seed `[]`, but again the inner item type is unknown.
71
+ for (let i = 0; i < segments.length; i++) {
72
+ if (typeof segments[i] !== "number") continue;
73
+ if (i + 1 >= segments.length) continue;
74
+ if (typeof segments[i + 1] !== "string") continue;
75
+
76
+ const partial = segments
77
+ .slice(0, i + 1)
78
+ .map((s) => String(s))
79
+ .join(".");
80
+ const fullPath = dataPath ? `${dataPath}.${partial}` : partial;
81
+
82
+ if (getProjectedValue(result, fullPath) === undefined) {
83
+ result = setProjectedValue(result, fullPath, {});
84
+ }
85
+ }
86
+ }
87
+
88
+ return result;
89
+ }
90
+
91
+ /**
92
+ * Minimal UI schema shape this utility traverses. Compatible with
93
+ * `@jsonforms/core`'s `UISchemaElement` but kept local to avoid a runtime
94
+ * dep on `@jsonforms/core` (it's a peer dep — types only).
95
+ */
96
+ export interface UISchemaLike {
97
+ type?: string;
98
+ scope?: string;
99
+ options?: { projection?: string; [key: string]: unknown };
100
+ elements?: UISchemaLike[];
101
+ [key: string]: unknown;
102
+ }
103
+
104
+ function collectProjectionControls(
105
+ ui: UISchemaLike | UISchemaLike[] | undefined,
106
+ out: { scope: string; projection: string }[],
107
+ ): void {
108
+ if (!ui) return;
109
+ if (Array.isArray(ui)) {
110
+ for (const el of ui) collectProjectionControls(el, out);
111
+ return;
112
+ }
113
+ if (typeof ui !== "object") return;
114
+
115
+ const projection = ui.options?.projection;
116
+ if (
117
+ ui.type === "Control" &&
118
+ typeof ui.scope === "string" &&
119
+ typeof projection === "string"
120
+ ) {
121
+ out.push({ scope: ui.scope, projection });
122
+ }
123
+
124
+ if (Array.isArray(ui.elements)) {
125
+ for (const el of ui.elements) collectProjectionControls(el, out);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Convert a JsonForms scope pointer (`#/properties/foo/properties/bar`) to
131
+ * a dot-separated data path (`foo.bar`). Drops `items` segments — array
132
+ * indices are added at runtime via the projection path, not the scope.
133
+ */
134
+ function scopeToDataPath(scope: string): string {
135
+ if (!scope.startsWith("#/")) return "";
136
+ const parts = scope.slice(2).split("/");
137
+ const out: string[] = [];
138
+ for (let i = 0; i < parts.length; i++) {
139
+ if (parts[i] === "properties" && i + 1 < parts.length) {
140
+ out.push(parts[++i]!);
141
+ }
142
+ }
143
+ return out.join(".");
144
+ }
@@ -14,30 +14,38 @@ export interface FlattenTransform extends Transform {
14
14
  labelFormat?: string; // Optional format string like "{parent.name} → {name}"
15
15
  }
16
16
 
17
+ export type FilterOperator =
18
+ | "eq"
19
+ | "neq"
20
+ | "empty"
21
+ | "notEmpty"
22
+ | "gt"
23
+ | "gte"
24
+ | "lt"
25
+ | "lte"
26
+ | "contains";
27
+
28
+ export interface FilterCondition {
29
+ key: string;
30
+ operator?: FilterOperator; // Defaults to "eq" when values is provided, "exists" behavior when neither
31
+ values?: unknown[];
32
+ }
33
+
17
34
  export interface FilterTransform extends Transform {
18
35
  name: "filter";
19
- key: string; // The key to check
20
- values?: unknown[]; // Optional array of values to match against
36
+ key?: string; // Legacy: single key to check
37
+ values?: unknown[]; // Legacy: single values array
38
+ conditions?: FilterCondition[]; // Multi-condition filter (AND logic)
21
39
  }
22
40
 
23
41
  export type TransformStep = FlattenTransform | FilterTransform;
24
42
 
25
43
  export type TransformPipeline = TransformStep[];
26
44
 
27
- /**
28
- * Registry of transform functions
29
- */
30
45
  type TransformFunction = (items: unknown[], config: Transform) => unknown[];
31
46
 
32
47
  const transformRegistry: Record<string, TransformFunction> = {};
33
48
 
34
- /**
35
- * Register a transform function
36
- */
37
- export function registerTransform(name: string, fn: TransformFunction): void {
38
- transformRegistry[name] = fn;
39
- }
40
-
41
49
  /**
42
50
  * Apply a pipeline of transforms to data
43
51
  */
@@ -124,29 +132,113 @@ function flattenTransform(items: unknown[], config: Transform): unknown[] {
124
132
  return flattened;
125
133
  }
126
134
 
135
+ function isEmpty(value: unknown): boolean {
136
+ if (value === null || value === undefined) return true;
137
+ if (Array.isArray(value)) return value.length === 0;
138
+ if (typeof value === "string") return value.length === 0;
139
+ return false;
140
+ }
141
+
142
+ function evaluateCondition(
143
+ itemObj: Record<string, unknown>,
144
+ condition: FilterCondition,
145
+ ): boolean {
146
+ const value = itemObj[condition.key];
147
+ const operator = condition.operator ?? (condition.values ? "eq" : "eq");
148
+
149
+ switch (operator) {
150
+ case "eq":
151
+ if (!condition.values || condition.values.length === 0) {
152
+ return condition.key in itemObj;
153
+ }
154
+ return condition.values.includes(value);
155
+ case "neq":
156
+ if (!condition.values || condition.values.length === 0) {
157
+ return !(condition.key in itemObj);
158
+ }
159
+ return !condition.values.includes(value);
160
+ case "empty":
161
+ return isEmpty(value);
162
+ case "notEmpty":
163
+ return !isEmpty(value);
164
+ case "gt":
165
+ return (
166
+ typeof value === "number" &&
167
+ condition.values !== undefined &&
168
+ value > (condition.values[0] as number)
169
+ );
170
+ case "gte":
171
+ return (
172
+ typeof value === "number" &&
173
+ condition.values !== undefined &&
174
+ value >= (condition.values[0] as number)
175
+ );
176
+ case "lt":
177
+ return (
178
+ typeof value === "number" &&
179
+ condition.values !== undefined &&
180
+ value < (condition.values[0] as number)
181
+ );
182
+ case "lte":
183
+ return (
184
+ typeof value === "number" &&
185
+ condition.values !== undefined &&
186
+ value <= (condition.values[0] as number)
187
+ );
188
+ case "contains":
189
+ if (typeof value === "string" && condition.values) {
190
+ return condition.values.some((v) => value.includes(String(v)));
191
+ }
192
+ if (Array.isArray(value) && condition.values) {
193
+ return condition.values.some((v) => value.includes(v));
194
+ }
195
+ return false;
196
+ default:
197
+ return false;
198
+ }
199
+ }
200
+
127
201
  /**
128
- * Filter transform - filters items based on a key and optional values
202
+ * Filter transform - filters items based on conditions
203
+ *
204
+ * Supports legacy single key/values syntax and new multi-condition syntax.
205
+ * When using conditions, all conditions must match (AND logic).
206
+ *
207
+ * Operators:
208
+ * eq - item[key] matches one of values (default)
209
+ * neq - item[key] does NOT match any of values
210
+ * empty - item[key] is null, undefined, empty array, or empty string
211
+ * notEmpty - inverse of empty
212
+ * gt - item[key] > values[0]
213
+ * gte - item[key] >= values[0]
214
+ * lt - item[key] < values[0]
215
+ * lte - item[key] <= values[0]
216
+ * contains - string includes substring, or array includes value
129
217
  */
130
218
  function filterTransform(items: unknown[], config: Transform): unknown[] {
131
219
  const filterConfig = config as FilterTransform;
132
- const { key, values } = filterConfig;
220
+
221
+ // Build conditions array from either new or legacy syntax
222
+ let conditions: FilterCondition[];
223
+
224
+ if (filterConfig.conditions) {
225
+ conditions = filterConfig.conditions;
226
+ } else if (filterConfig.key) {
227
+ // Legacy single key/values syntax
228
+ conditions = [{ key: filterConfig.key, values: filterConfig.values }];
229
+ } else {
230
+ return items;
231
+ }
133
232
 
134
233
  return items.filter((item) => {
135
234
  if (typeof item !== "object" || item === null) return false;
136
-
137
235
  const itemObj = item as Record<string, unknown>;
138
-
139
- // If no values array provided, just check if the key exists
140
- if (!values || values.length === 0) {
141
- return key in itemObj;
142
- }
143
-
144
- // If values array provided, check if item[key] matches any of the values
145
- const itemValue = itemObj[key];
146
- return values.includes(itemValue);
236
+ return conditions.every((condition) =>
237
+ evaluateCondition(itemObj, condition),
238
+ );
147
239
  });
148
240
  }
149
241
 
150
242
  // Register built-in transforms
151
- registerTransform("flatten", flattenTransform);
152
- registerTransform("filter", filterTransform);
243
+ transformRegistry["flatten"] = flattenTransform;
244
+ transformRegistry["filter"] = filterTransform;