@narrative.io/jsonforms-provider-protocols 2.10.0 → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +166 -30
  2. package/dist/core/projection.d.ts +32 -0
  3. package/dist/core/projection.d.ts.map +1 -0
  4. package/dist/core/projection.js +74 -0
  5. package/dist/core/projection.js.map +1 -0
  6. package/dist/core/resolveScope.d.ts +11 -0
  7. package/dist/core/resolveScope.d.ts.map +1 -0
  8. package/dist/core/resolveScope.js +22 -0
  9. package/dist/core/resolveScope.js.map +1 -0
  10. package/dist/core/transforms.d.ts +8 -10
  11. package/dist/core/transforms.d.ts.map +1 -1
  12. package/dist/core/transforms.js +56 -13
  13. package/dist/core/transforms.js.map +1 -1
  14. package/dist/core/types.d.ts +7 -0
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +12 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/jsonforms-provider-protocols.css +2 -2
  21. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  22. package/dist/vue/components/ProviderAutocomplete.vue.js +4 -2
  23. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  24. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  25. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  26. package/dist/vue/components/ProviderMultiSelect.vue2.js +6 -4
  27. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  28. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  29. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  30. package/dist/vue/components/ProviderSelect.vue2.js +5 -3
  31. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  32. package/dist/vue/composables/useDataLayer.d.ts +9 -0
  33. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  34. package/dist/vue/composables/useDataLayer.js +25 -0
  35. package/dist/vue/composables/useDataLayer.js.map +1 -0
  36. package/dist/vue/composables/useDerive.d.ts +5 -2
  37. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  38. package/dist/vue/composables/useDerive.js +12 -12
  39. package/dist/vue/composables/useDerive.js.map +1 -1
  40. package/dist/vue/composables/useProjection.d.ts +35 -0
  41. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  42. package/dist/vue/composables/useProjection.js +33 -0
  43. package/dist/vue/composables/useProjection.js.map +1 -0
  44. package/dist/vue/index.d.ts +4 -0
  45. package/dist/vue/index.d.ts.map +1 -1
  46. package/dist/vue/index.js +15 -29
  47. package/dist/vue/index.js.map +1 -1
  48. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  49. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  50. package/dist/vue/primevue/JfBoolean.vue.js +10 -5
  51. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  52. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  53. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  54. package/dist/vue/primevue/JfEnum.vue.js +12 -7
  55. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  56. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  57. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  58. package/dist/vue/primevue/JfEnumArray.vue.js +13 -8
  59. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  60. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  61. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  62. package/dist/vue/primevue/JfNumber.vue.js +11 -6
  63. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  64. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  65. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  66. package/dist/vue/primevue/JfText.vue.js +13 -8
  67. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  68. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  69. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  70. package/dist/vue/primevue/JfTextArea.vue.js +11 -6
  71. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  72. package/dist/vue/primevue/index.d.ts.map +1 -1
  73. package/dist/vue/primevue/index.js +22 -7
  74. package/dist/vue/primevue/index.js.map +1 -1
  75. package/package.json +7 -3
  76. package/src/core/projection.ts +136 -0
  77. package/src/core/resolveScope.ts +39 -0
  78. package/src/core/transforms.ts +91 -26
  79. package/src/core/types.ts +8 -0
  80. package/src/index.ts +7 -0
  81. package/src/vue/components/ProviderAutocomplete.vue +4 -2
  82. package/src/vue/components/ProviderMultiSelect.vue +6 -4
  83. package/src/vue/components/ProviderSelect.vue +5 -3
  84. package/src/vue/composables/useDataLayer.ts +43 -0
  85. package/src/vue/composables/useDerive.ts +19 -16
  86. package/src/vue/composables/useProjection.ts +74 -0
  87. package/src/vue/index.ts +20 -46
  88. package/src/vue/primevue/JfBoolean.vue +7 -2
  89. package/src/vue/primevue/JfEnum.vue +9 -4
  90. package/src/vue/primevue/JfEnumArray.vue +10 -5
  91. package/src/vue/primevue/JfNumber.vue +8 -3
  92. package/src/vue/primevue/JfText.vue +10 -5
  93. package/src/vue/primevue/JfTextArea.vue +8 -3
  94. package/src/vue/primevue/index.ts +32 -7
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Projection utilities for navigating complex data structures
3
+ * through a dot-separated path where numeric segments are array indices.
4
+ *
5
+ * Examples:
6
+ * "0" → first element of an array
7
+ * "include" → the `include` property of an object
8
+ * "0.video_rate_usd" → nested property inside the first array element
9
+ */
10
+
11
+ export type ProjectionSegment = string | number;
12
+
13
+ /**
14
+ * Parse a projection path string into typed segments.
15
+ * Numeric strings become numbers (array indices), others stay as strings (object keys).
16
+ */
17
+ export function parseProjectionPath(path: string): ProjectionSegment[] {
18
+ if (!path) return [];
19
+ return path.split(".").map((s) => {
20
+ const n = Number(s);
21
+ return Number.isInteger(n) && n >= 0 ? n : s;
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Read a value from `data` by following the projection path.
27
+ * Returns `undefined` if any segment along the path is missing.
28
+ */
29
+ export function getProjectedValue(
30
+ data: unknown,
31
+ path: string,
32
+ ): unknown {
33
+ const segments = parseProjectionPath(path);
34
+ let current: unknown = data;
35
+
36
+ for (const seg of segments) {
37
+ if (current === null || current === undefined) return undefined;
38
+
39
+ if (typeof seg === "number") {
40
+ if (!Array.isArray(current)) return undefined;
41
+ current = current[seg];
42
+ } else {
43
+ if (typeof current !== "object") return undefined;
44
+ current = (current as Record<string, unknown>)[seg];
45
+ }
46
+ }
47
+
48
+ return current;
49
+ }
50
+
51
+ /**
52
+ * Immutably set a value at the projection path, preserving all sibling data.
53
+ * Constructs missing intermediate structures (arrays for numeric segments, objects for string segments).
54
+ */
55
+ export function setProjectedValue(
56
+ data: unknown,
57
+ path: string,
58
+ value: unknown,
59
+ ): unknown {
60
+ const segments = parseProjectionPath(path);
61
+ return setAtPath(data, segments, 0, value);
62
+ }
63
+
64
+ function setAtPath(
65
+ current: unknown,
66
+ segments: ProjectionSegment[],
67
+ index: number,
68
+ value: unknown,
69
+ ): unknown {
70
+ if (index === segments.length) {
71
+ return value;
72
+ }
73
+
74
+ const seg = segments[index]!;
75
+
76
+ if (typeof seg === "number") {
77
+ // Array index — ensure we have an array
78
+ const arr = Array.isArray(current) ? [...current] : [];
79
+ // Pad array if index is out of bounds
80
+ while (arr.length <= seg) {
81
+ arr.push(undefined);
82
+ }
83
+ arr[seg] = setAtPath(arr[seg], segments, index + 1, value);
84
+ return arr;
85
+ } else {
86
+ // Object key — ensure we have an object
87
+ const obj: Record<string, unknown> =
88
+ current !== null && current !== undefined && typeof current === "object" && !Array.isArray(current)
89
+ ? { ...(current as Record<string, unknown>) }
90
+ : {};
91
+ obj[seg] = setAtPath(obj[seg], segments, index + 1, value);
92
+ return obj;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve the schema at the projected path.
98
+ * Numeric segments traverse into `items` (array item schema).
99
+ * String segments traverse into `properties[segment]`.
100
+ */
101
+ export function getProjectedSchema(
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ schema: Record<string, any>,
104
+ path: string,
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ ): Record<string, any> {
107
+ const segments = parseProjectionPath(path);
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ let current: Record<string, any> = schema;
110
+
111
+ for (const seg of segments) {
112
+ if (!current) return {};
113
+
114
+ if (typeof seg === "number") {
115
+ // Array index → traverse into items schema
116
+ const items = current.items;
117
+ if (items && typeof items === "object") {
118
+ current = items as Record<string, unknown>;
119
+ } else {
120
+ return {};
121
+ }
122
+ } else {
123
+ // Object key → traverse into properties[key]
124
+ const properties = current.properties as
125
+ | Record<string, Record<string, unknown>>
126
+ | undefined;
127
+ if (properties && properties[seg]) {
128
+ current = properties[seg];
129
+ } else {
130
+ return {};
131
+ }
132
+ }
133
+ }
134
+
135
+ return current;
136
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Resolve a JSON Forms scope path to its schema within a root schema.
3
+ * Handles nested paths like "#/properties/parent/properties/child".
4
+ *
5
+ * Follows JSON Schema structure:
6
+ * - "properties" segments navigate into object `.properties`
7
+ * - "items" segments navigate into array `.items`
8
+ * - all other segments index directly into the current object
9
+ */
10
+ export function resolveScopeSchema(
11
+ scope: string,
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ rootSchema: Record<string, any>,
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ ): Record<string, any> | undefined {
16
+ if (!scope || !rootSchema) return undefined;
17
+
18
+ // Remove the leading "#/" and split into segments
19
+ const path = scope.replace(/^#\/?/, "");
20
+ if (!path) return rootSchema;
21
+
22
+ const segments = path.split("/");
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ let current: any = rootSchema;
25
+
26
+ for (const segment of segments) {
27
+ if (!current || typeof current !== "object") return undefined;
28
+
29
+ if (segment === "properties") {
30
+ current = current.properties;
31
+ } else if (segment === "items") {
32
+ current = current.items;
33
+ } else {
34
+ current = current[segment];
35
+ }
36
+ }
37
+
38
+ return current;
39
+ }
@@ -14,30 +14,29 @@ export interface FlattenTransform extends Transform {
14
14
  labelFormat?: string; // Optional format string like "{parent.name} → {name}"
15
15
  }
16
16
 
17
+ export type FilterOperator = "eq" | "neq" | "empty" | "notEmpty" | "gt" | "gte" | "lt" | "lte" | "contains";
18
+
19
+ export interface FilterCondition {
20
+ key: string;
21
+ operator?: FilterOperator; // Defaults to "eq" when values is provided, "exists" behavior when neither
22
+ values?: unknown[];
23
+ }
24
+
17
25
  export interface FilterTransform extends Transform {
18
26
  name: "filter";
19
- key: string; // The key to check
20
- values?: unknown[]; // Optional array of values to match against
27
+ key?: string; // Legacy: single key to check
28
+ values?: unknown[]; // Legacy: single values array
29
+ conditions?: FilterCondition[]; // Multi-condition filter (AND logic)
21
30
  }
22
31
 
23
32
  export type TransformStep = FlattenTransform | FilterTransform;
24
33
 
25
34
  export type TransformPipeline = TransformStep[];
26
35
 
27
- /**
28
- * Registry of transform functions
29
- */
30
36
  type TransformFunction = (items: unknown[], config: Transform) => unknown[];
31
37
 
32
38
  const transformRegistry: Record<string, TransformFunction> = {};
33
39
 
34
- /**
35
- * Register a transform function
36
- */
37
- export function registerTransform(name: string, fn: TransformFunction): void {
38
- transformRegistry[name] = fn;
39
- }
40
-
41
40
  /**
42
41
  * Apply a pipeline of transforms to data
43
42
  */
@@ -124,29 +123,95 @@ function flattenTransform(items: unknown[], config: Transform): unknown[] {
124
123
  return flattened;
125
124
  }
126
125
 
126
+ function isEmpty(value: unknown): boolean {
127
+ if (value === null || value === undefined) return true;
128
+ if (Array.isArray(value)) return value.length === 0;
129
+ if (typeof value === "string") return value.length === 0;
130
+ return false;
131
+ }
132
+
133
+ function evaluateCondition(
134
+ itemObj: Record<string, unknown>,
135
+ condition: FilterCondition,
136
+ ): boolean {
137
+ const value = itemObj[condition.key];
138
+ const operator = condition.operator ?? (condition.values ? "eq" : "eq");
139
+
140
+ switch (operator) {
141
+ case "eq":
142
+ if (!condition.values || condition.values.length === 0) {
143
+ return condition.key in itemObj;
144
+ }
145
+ return condition.values.includes(value);
146
+ case "neq":
147
+ if (!condition.values || condition.values.length === 0) {
148
+ return !(condition.key in itemObj);
149
+ }
150
+ return !condition.values.includes(value);
151
+ case "empty":
152
+ return isEmpty(value);
153
+ case "notEmpty":
154
+ return !isEmpty(value);
155
+ case "gt":
156
+ return typeof value === "number" && condition.values !== undefined && value > (condition.values[0] as number);
157
+ case "gte":
158
+ return typeof value === "number" && condition.values !== undefined && value >= (condition.values[0] as number);
159
+ case "lt":
160
+ return typeof value === "number" && condition.values !== undefined && value < (condition.values[0] as number);
161
+ case "lte":
162
+ return typeof value === "number" && condition.values !== undefined && value <= (condition.values[0] as number);
163
+ case "contains":
164
+ if (typeof value === "string" && condition.values) {
165
+ return condition.values.some((v) => value.includes(String(v)));
166
+ }
167
+ if (Array.isArray(value) && condition.values) {
168
+ return condition.values.some((v) => value.includes(v));
169
+ }
170
+ return false;
171
+ default:
172
+ return false;
173
+ }
174
+ }
175
+
127
176
  /**
128
- * Filter transform - filters items based on a key and optional values
177
+ * Filter transform - filters items based on conditions
178
+ *
179
+ * Supports legacy single key/values syntax and new multi-condition syntax.
180
+ * When using conditions, all conditions must match (AND logic).
181
+ *
182
+ * Operators:
183
+ * eq - item[key] matches one of values (default)
184
+ * neq - item[key] does NOT match any of values
185
+ * empty - item[key] is null, undefined, empty array, or empty string
186
+ * notEmpty - inverse of empty
187
+ * gt - item[key] > values[0]
188
+ * gte - item[key] >= values[0]
189
+ * lt - item[key] < values[0]
190
+ * lte - item[key] <= values[0]
191
+ * contains - string includes substring, or array includes value
129
192
  */
130
193
  function filterTransform(items: unknown[], config: Transform): unknown[] {
131
194
  const filterConfig = config as FilterTransform;
132
- const { key, values } = filterConfig;
195
+
196
+ // Build conditions array from either new or legacy syntax
197
+ let conditions: FilterCondition[];
198
+
199
+ if (filterConfig.conditions) {
200
+ conditions = filterConfig.conditions;
201
+ } else if (filterConfig.key) {
202
+ // Legacy single key/values syntax
203
+ conditions = [{ key: filterConfig.key, values: filterConfig.values }];
204
+ } else {
205
+ return items;
206
+ }
133
207
 
134
208
  return items.filter((item) => {
135
209
  if (typeof item !== "object" || item === null) return false;
136
-
137
210
  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);
211
+ return conditions.every((condition) => evaluateCondition(itemObj, condition));
147
212
  });
148
213
  }
149
214
 
150
215
  // Register built-in transforms
151
- registerTransform("flatten", flattenTransform);
152
- registerTransform("filter", filterTransform);
216
+ transformRegistry["flatten"] = flattenTransform;
217
+ transformRegistry["filter"] = filterTransform;
package/src/core/types.ts CHANGED
@@ -44,3 +44,11 @@ export interface AuthConfig {
44
44
  token?: string | (() => string); // Generic token auth
45
45
  [key: string]: unknown; // Custom auth fields
46
46
  }
47
+
48
+ export interface ConnectorDataLayer {
49
+ dataset_name?: string;
50
+ dataset_description?: string;
51
+ dataset_id?: number;
52
+ profile_id?: string;
53
+ profile_name?: string;
54
+ }
package/src/index.ts CHANGED
@@ -3,11 +3,15 @@ import { cache as globalCache } from "./core/cache";
3
3
  import { registry as globalRegistry } from "./core/registry";
4
4
  import type { Protocol, AuthConfig } from "./core/types";
5
5
 
6
+ console.log("[jsonforms-provider-protocols] loaded v2.10.0 (symlink)");
7
+
6
8
  export { cache } from "./core/cache";
7
9
  export * from "./core/jsonpath";
8
10
  export { registry } from "./core/registry";
9
11
  export * from "./core/templating";
10
12
  export * from "./core/transforms";
13
+ export * from "./core/projection";
14
+ export * from "./core/resolveScope";
11
15
  // Core exports
12
16
  export * from "./core/types";
13
17
 
@@ -22,6 +26,8 @@ export {
22
26
  ProviderSelect,
23
27
  ProviderMultiSelect,
24
28
  useProvider,
29
+ createDataLayer,
30
+ useDataLayer,
25
31
  JfText,
26
32
  JfTextArea,
27
33
  JfNumber,
@@ -29,6 +35,7 @@ export {
29
35
  JfEnumArray,
30
36
  JfBoolean,
31
37
  } from "./vue";
38
+ export type { DataLayer } from "./vue";
32
39
 
33
40
  export interface ProviderConfig {
34
41
  protocols?: Protocol[];
@@ -3,6 +3,7 @@ import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { useJsonFormsControl } from "@jsonforms/vue";
4
4
  import { computed, ref, watch } from "vue";
5
5
  import { useProvider } from "../composables/useProvider";
6
+ import { useProjection } from "../composables/useProjection";
6
7
  import AutoComplete from "primevue/autocomplete";
7
8
 
8
9
  const props = defineProps<{
@@ -10,7 +11,8 @@ const props = defineProps<{
10
11
  schema: JsonSchema;
11
12
  path: string;
12
13
  }>();
13
- const { control, handleChange } = useJsonFormsControl(props);
14
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
15
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
14
16
 
15
17
  const binding = computed(() => {
16
18
  const provider = control.value.uischema?.options?.provider;
@@ -33,7 +35,7 @@ watch(query, () => {
33
35
  });
34
36
 
35
37
  const value = computed({
36
- get: () => control.value.data,
38
+ get: () => projectedData.value,
37
39
  set: (v) => handleChange(control.value.path, v),
38
40
  });
39
41
 
@@ -3,6 +3,7 @@ import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { useJsonFormsControl } from "@jsonforms/vue";
4
4
  import { computed, inject, watch } from "vue";
5
5
  import { useProvider } from "../composables/useProvider";
6
+ import { useProjection } from "../composables/useProjection";
6
7
  import { shouldAutoSelectMulti } from "../utils/autoSelect";
7
8
  import MultiSelect from "primevue/multiselect";
8
9
 
@@ -11,7 +12,8 @@ const props = defineProps<{
11
12
  schema: JsonSchema;
12
13
  path: string;
13
14
  }>();
14
- const { control, handleChange } = useJsonFormsControl(props);
15
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
16
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
15
17
 
16
18
  const binding = computed(() => {
17
19
  const provider = control.value.uischema?.options?.provider;
@@ -53,7 +55,7 @@ watch(
53
55
  control.value.uischema?.options?.autoSelectSingle === true,
54
56
  isLoading,
55
57
  items: newItems,
56
- currentValue: Array.isArray(control.value.data) ? control.value.data : [],
58
+ currentValue: Array.isArray(projectedData.value) ? projectedData.value : [],
57
59
  });
58
60
 
59
61
  if (valueToSelect !== null) {
@@ -74,13 +76,13 @@ const sameSet = (a: unknown[], b: unknown[]) => {
74
76
  // v-model with guard to avoid recursive updates
75
77
  const value = computed({
76
78
  get() {
77
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
79
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
78
80
  // return a fresh copy so MultiSelect can't mutate JSONForms' array in place
79
81
  return [...curr];
80
82
  },
81
83
  set(val) {
82
84
  const next = Array.isArray(val) ? [...val] : [];
83
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
85
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
84
86
  if (!sameSet(curr, next)) handleChange(control.value.path, next);
85
87
  },
86
88
  });
@@ -3,6 +3,7 @@ import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { useJsonFormsControl } from "@jsonforms/vue";
4
4
  import { computed, inject, watch } from "vue";
5
5
  import { useProvider } from "../composables/useProvider";
6
+ import { useProjection } from "../composables/useProjection";
6
7
  import { shouldAutoSelect } from "../utils/autoSelect";
7
8
  import Dropdown from "primevue/dropdown";
8
9
 
@@ -11,7 +12,8 @@ const props = defineProps<{
11
12
  schema: JsonSchema;
12
13
  path: string;
13
14
  }>();
14
- const { control, handleChange } = useJsonFormsControl(props);
15
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
16
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
15
17
 
16
18
  const binding = computed(() => {
17
19
  const provider = control.value.uischema?.options?.provider;
@@ -53,7 +55,7 @@ watch(
53
55
  control.value.uischema?.options?.autoSelectSingle !== false,
54
56
  isLoading,
55
57
  items: newItems,
56
- currentValue: control.value.data,
58
+ currentValue: projectedData.value,
57
59
  });
58
60
 
59
61
  if (valueToSelect !== null) {
@@ -64,7 +66,7 @@ watch(
64
66
  );
65
67
 
66
68
  const value = computed({
67
- get: () => control.value.data,
69
+ get: () => projectedData.value,
68
70
  set: (v) => handleChange(control.value.path, v),
69
71
  });
70
72
 
@@ -0,0 +1,43 @@
1
+ import {
2
+ ref,
3
+ provide,
4
+ inject,
5
+ readonly,
6
+ type DeepReadonly,
7
+ type Ref,
8
+ } from "vue";
9
+ import type { ConnectorDataLayer } from "../../core/types";
10
+
11
+ const DATA_LAYER_KEY = Symbol("dataLayer");
12
+
13
+ export interface DataLayer {
14
+ push(data: Partial<ConnectorDataLayer>): void;
15
+ state: DeepReadonly<Ref<ConnectorDataLayer>>;
16
+ }
17
+
18
+ export function createDataLayer(): DataLayer {
19
+ const state = ref<ConnectorDataLayer>({});
20
+
21
+ const dataLayer: DataLayer = {
22
+ push(data: Partial<ConnectorDataLayer>) {
23
+ state.value = { ...state.value, ...data };
24
+ },
25
+ state: readonly(state) as DeepReadonly<Ref<ConnectorDataLayer>>,
26
+ };
27
+
28
+ provide(DATA_LAYER_KEY, dataLayer);
29
+
30
+ return dataLayer;
31
+ }
32
+
33
+ export function useDataLayer(): DeepReadonly<Ref<ConnectorDataLayer>> {
34
+ const dataLayer = inject<DataLayer | null>(DATA_LAYER_KEY, null);
35
+ if (dataLayer) {
36
+ return dataLayer.state;
37
+ }
38
+
39
+ // No dataLayer provided — return empty reactive ref
40
+ return readonly(ref<ConnectorDataLayer>({})) as DeepReadonly<
41
+ Ref<ConnectorDataLayer>
42
+ >;
43
+ }
@@ -1,5 +1,6 @@
1
- import { computed, watch, inject, type Ref } from "vue";
1
+ import { computed, watch, unref, inject, type Ref, type ComputedRef } from "vue";
2
2
  import { type ControlElement } from "@jsonforms/core";
3
+ import { useDataLayer } from "./useDataLayer";
3
4
 
4
5
  interface DeriveOptions {
5
6
  control: Ref<{
@@ -8,20 +9,21 @@ interface DeriveOptions {
8
9
  data: unknown;
9
10
  }>;
10
11
  handleChange: (path: string, value: unknown) => void;
12
+ /** When projection is active, pass projectedData so the comparison
13
+ * matches the projected (unwrapped) value rather than raw scope data. */
14
+ data?: Ref<unknown> | ComputedRef<unknown>;
11
15
  }
12
16
 
13
- export function useDerive({ control, handleChange }: DeriveOptions) {
17
+ export function useDerive({ control, handleChange, data: dataOverride }: DeriveOptions) {
14
18
  // Get the root form data from JSONForms context
15
19
  const injectedFormData = inject<{ value: unknown }>("formData", {
16
20
  value: {},
17
21
  });
18
22
  const rootData = computed(() => injectedFormData.value || {});
19
23
 
20
- // Get external data from context if available
21
- const injectedExternalData = inject<{ value: unknown }>("externalData", {
22
- value: {},
23
- });
24
- const externalData = computed(() => injectedExternalData.value || {});
24
+ // Get data from the dataLayer
25
+ const dataLayerState = useDataLayer();
26
+ const dataLayerData = computed(() => dataLayerState.value || {});
25
27
 
26
28
  // Extract derive configuration from uischema options
27
29
  const deriveConfig = computed(() => {
@@ -34,9 +36,9 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
34
36
  };
35
37
  });
36
38
 
37
- // Watch for changes in form data and external data and update derived field
39
+ // Watch for changes in form data and dataLayer and update derived field
38
40
  watch(
39
- [rootData, externalData, deriveConfig],
41
+ [rootData, dataLayerData, deriveConfig],
40
42
  ([data, extData, config]) => {
41
43
  if (!config.expression || config.mode !== "follow") {
42
44
  return;
@@ -48,7 +50,8 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
48
50
  data,
49
51
  extData,
50
52
  );
51
- if (derivedValue !== control.value.data) {
53
+ const compareData = dataOverride ? unref(dataOverride) : control.value.data;
54
+ if (derivedValue !== compareData) {
52
55
  handleChange(control.value.path, derivedValue);
53
56
  }
54
57
  } catch (error) {
@@ -65,12 +68,12 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
65
68
  function resolveDeriveExpression(
66
69
  expression: string,
67
70
  data: unknown,
68
- externalData?: unknown,
71
+ dataLayerData?: unknown,
69
72
  ): unknown {
70
- // Handle externalData() syntax
71
- if (expression.startsWith("externalData(") && expression.endsWith(")")) {
72
- const propertyPath = expression.slice(13, -1); // Remove "externalData(" and ")"
73
- return resolvePropertyPath(propertyPath, externalData);
73
+ // Handle dataLayer() syntax
74
+ if (expression.startsWith("dataLayer(") && expression.endsWith(")")) {
75
+ const propertyPath = expression.slice(10, -1); // Remove "dataLayer(" and ")"
76
+ return resolvePropertyPath(propertyPath, dataLayerData);
74
77
  }
75
78
 
76
79
  // Handle simple property paths like "country.name"
@@ -82,7 +85,7 @@ function resolveDeriveExpression(
82
85
  return resolvePropertyPath(expression, data);
83
86
  }
84
87
 
85
- // For now, we'll only support simple property paths and externalData() calls
88
+ // For now, we'll only support simple property paths and dataLayer() calls
86
89
  // Complex expressions would require a safe expression evaluator
87
90
  return resolvePropertyPath(expression, data);
88
91
  }
@@ -0,0 +1,74 @@
1
+ import { computed, type ComputedRef, type Ref } from "vue";
2
+ import {
3
+ getProjectedValue,
4
+ setProjectedValue,
5
+ getProjectedSchema,
6
+ } from "../../core/projection";
7
+
8
+ interface ProjectionControl {
9
+ data: unknown;
10
+ path: string;
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ schema: Record<string, any>;
13
+ uischema: { options?: { projection?: string; [key: string]: unknown } };
14
+ }
15
+
16
+ export interface ProjectionResult {
17
+ /** The value at the projected path (for rendering) */
18
+ projectedData: ComputedRef<unknown>;
19
+ /** The schema at the projected path (for renderer selection) */
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ projectedSchema: ComputedRef<Record<string, any>>;
22
+ /** Wrapped handleChange that writes through the projection */
23
+ handleProjectedChange: (path: string, value: unknown) => void;
24
+ /** Whether projection is active */
25
+ hasProjection: boolean;
26
+ }
27
+
28
+ /**
29
+ * Composable that wraps a JSON Forms control with projection support.
30
+ *
31
+ * When `options.projection` is set on the uischema, this composable:
32
+ * - Reads the projected sub-value from the control data
33
+ * - Resolves the projected sub-schema for renderer type resolution
34
+ * - Wraps handleChange to write back through the projection path (preserving siblings)
35
+ *
36
+ * When no projection is set, it passes through control data/schema/handleChange unchanged.
37
+ */
38
+ export function useProjection(
39
+ control: Ref<ProjectionControl>,
40
+ handleChange: (path: string, value: unknown) => void,
41
+ ): ProjectionResult {
42
+ const projection = control.value.uischema?.options?.projection as
43
+ | string
44
+ | undefined;
45
+
46
+ if (!projection) {
47
+ return {
48
+ projectedData: computed(() => control.value.data),
49
+ projectedSchema: computed(() => control.value.schema),
50
+ handleProjectedChange: handleChange,
51
+ hasProjection: false,
52
+ };
53
+ }
54
+
55
+ const projectedData = computed(() =>
56
+ getProjectedValue(control.value.data, projection),
57
+ );
58
+
59
+ const projectedSchema = computed(() =>
60
+ getProjectedSchema(control.value.schema, projection),
61
+ );
62
+
63
+ const handleProjectedChange = (path: string, value: unknown) => {
64
+ const fullValue = setProjectedValue(control.value.data, projection, value);
65
+ handleChange(path, fullValue);
66
+ };
67
+
68
+ return {
69
+ projectedData,
70
+ projectedSchema,
71
+ handleProjectedChange,
72
+ hasProjection: true,
73
+ };
74
+ }