@workbench-kit/field-remap 0.0.1-prototype.0 → 0.0.2-prototype.0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,187 +1,187 @@
1
- import type { ConversionDefinition } from './conversionDefinition.js';
2
- import {
3
- mergeSourceShapes,
4
- targetSlotsFromShape,
5
- type DataShape,
6
- type DataShapeRegistry,
7
- } from './dataShape.js';
8
- import { convertArrayWithItemEdges } from '../mapping/convertItemEdges.js';
9
- import { isPlainObject, readObjectPath, writeObjectPath } from '../mapping/pathUtils.js';
10
- import { findSourceField, resolveMappedValue } from '../mapping/resolveMappedValue.js';
11
- import { findTargetSlot, flattenTargetSlots } from '../mapping/treeUtils.js';
12
- import type {
13
- SourceField,
14
- TargetSlot,
15
- TransformContext,
16
- ValueTransformRegistry,
17
- } from '../types.js';
18
-
19
- export interface ConvertToShapeInput {
20
- readonly conversion: ConversionDefinition;
21
- /** Shape registry, or an explicit list containing at least the referenced shapes. */
22
- readonly shapes: DataShapeRegistry | readonly DataShape[];
23
- /**
24
- * Named input bags keyed by source shape id.
25
- * Example: `{ order: liveOrder, customer: liveCustomer }`.
26
- */
27
- readonly inputs: Readonly<Record<string, unknown>>;
28
- readonly transforms: ValueTransformRegistry;
29
- readonly context?: TransformContext;
30
- }
31
-
32
- export interface ConvertToShapeSlotResult {
33
- readonly edgeId: string;
34
- readonly targetSlotId: string;
35
- readonly path: string;
36
- readonly value: unknown;
37
- }
38
-
39
- export interface ConvertToShapeResult {
40
- /** Nested JSON matching target shape paths. */
41
- readonly output: Record<string, unknown>;
42
- /** Flat per-slot results (debug / UI panels). */
43
- readonly slots: readonly ConvertToShapeSlotResult[];
44
- }
45
-
46
- function isDataShapeRegistry(
47
- shapes: DataShapeRegistry | readonly DataShape[],
48
- ): shapes is DataShapeRegistry {
49
- return typeof (shapes as DataShapeRegistry).get === 'function';
50
- }
51
-
52
- function resolveShape(
53
- shapes: DataShapeRegistry | readonly DataShape[],
54
- id: string,
55
- ): DataShape | undefined {
56
- if (isDataShapeRegistry(shapes)) {
57
- return shapes.get(id);
58
- }
59
- return shapes.find((shape) => shape.id === id);
60
- }
61
-
62
- function outputPathForTarget(slot: TargetSlot): string {
63
- const path = slot.path?.trim();
64
- if (path) {
65
- return path;
66
- }
67
- // Prefer last id segment when hosts use dotted ids (`slot.display.timeText`).
68
- const id = slot.id.trim();
69
- if (id.includes('.')) {
70
- const parts = id.split('.').filter(Boolean);
71
- // Drop a leading registry prefix like `slot` when present with 3+ segments.
72
- if (parts.length >= 3 && (parts[0] === 'slot' || parts[0] === 'tgt')) {
73
- return parts.slice(1).join('.');
74
- }
75
- if (parts.length >= 2) {
76
- return parts.slice(1).join('.');
77
- }
78
- }
79
- return slot.label.trim() || id;
80
- }
81
-
82
- function readFieldValue(field: SourceField, inputs: Readonly<Record<string, unknown>>): unknown {
83
- const shapeId = field.shapeId?.trim();
84
- const bag =
85
- shapeId && Object.prototype.hasOwnProperty.call(inputs, shapeId)
86
- ? inputs[shapeId]
87
- : // Single-bag fallback: use the only input, or the whole inputs record.
88
- Object.keys(inputs).length === 1
89
- ? inputs[Object.keys(inputs)[0]!]
90
- : inputs;
91
-
92
- const path = field.path?.trim();
93
- if (path) {
94
- // When bags are per-shape, paths are relative to that bag.
95
- // When falling back to the whole inputs record, absolute paths still work.
96
- if (shapeId && Object.prototype.hasOwnProperty.call(inputs, shapeId)) {
97
- return readObjectPath(bag, path);
98
- }
99
- const fromBag = readObjectPath(bag, path);
100
- if (fromBag !== undefined) {
101
- return fromBag;
102
- }
103
- // Absolute path into combined inputs (e.g. `order.totalCents`).
104
- return readObjectPath(inputs, path);
105
- }
106
-
107
- return field.sampleValue;
108
- }
109
-
110
- /**
111
- * Apply a conversion to named input bags and build target-shaped JSON.
112
- *
113
- * This is the host runtime entry point — not `sourceShape.convert(target, data)`.
114
- * Multiple source shapes are supported via `inputs[shapeId]`.
115
- */
116
- export function convertToShape(input: ConvertToShapeInput): ConvertToShapeResult {
117
- const sourceShapes: DataShape[] = [];
118
- for (const shapeId of input.conversion.sourceShapeIds) {
119
- const shape = resolveShape(input.shapes, shapeId);
120
- if (!shape) {
121
- throw new Error(`Unknown source shape "${shapeId}" for conversion "${input.conversion.id}".`);
122
- }
123
- if (shape.role === 'target') {
124
- throw new Error(`Shape "${shapeId}" has role "target" and cannot be used as a source.`);
125
- }
126
- sourceShapes.push(shape);
127
- }
128
-
129
- const targetShape = resolveShape(input.shapes, input.conversion.targetShapeId);
130
- if (!targetShape) {
131
- throw new Error(
132
- `Unknown target shape "${input.conversion.targetShapeId}" for conversion "${input.conversion.id}".`,
133
- );
134
- }
135
-
136
- const sources = mergeSourceShapes(sourceShapes);
137
- const targets = targetSlotsFromShape(targetShape);
138
- const targetLeaves = flattenTargetSlots(targets);
139
-
140
- let output: Record<string, unknown> = {};
141
- const slots: ConvertToShapeSlotResult[] = [];
142
-
143
- for (const edge of input.conversion.document.edges) {
144
- const sourceField = findSourceField(sources, edge.sourceFieldId);
145
- if (!sourceField) {
146
- continue;
147
- }
148
- const targetSlot =
149
- findTargetSlot(targets, edge.targetSlotId) ??
150
- targetLeaves.find((slot) => slot.id === edge.targetSlotId);
151
- if (!targetSlot) {
152
- continue;
153
- }
154
-
155
- const sourceValue = readFieldValue(sourceField, input.inputs);
156
- const value =
157
- edge.itemEdges && edge.itemEdges.length > 0
158
- ? convertArrayWithItemEdges({
159
- items: sourceValue,
160
- itemEdges: edge.itemEdges,
161
- sources,
162
- targets,
163
- transforms: input.transforms,
164
- context: input.context,
165
- })
166
- : resolveMappedValue(edge, sourceValue, input.transforms, {
167
- ...input.context,
168
- sampleValue: sourceValue,
169
- record: isPlainObject(sourceValue)
170
- ? sourceValue
171
- : isPlainObject(input.context?.record)
172
- ? input.context.record
173
- : undefined,
174
- });
175
-
176
- const path = outputPathForTarget(targetSlot);
177
- output = writeObjectPath(output, path, value);
178
- slots.push({
179
- edgeId: edge.id,
180
- targetSlotId: edge.targetSlotId,
181
- path,
182
- value,
183
- });
184
- }
185
-
186
- return { output, slots };
187
- }
1
+ import type { ConversionDefinition } from './conversionDefinition.js';
2
+ import {
3
+ mergeSourceShapes,
4
+ targetSlotsFromShape,
5
+ type DataShape,
6
+ type DataShapeRegistry,
7
+ } from './dataShape.js';
8
+ import { convertArrayWithItemEdges } from '../mapping/convertItemEdges.js';
9
+ import { isPlainObject, readObjectPath, writeObjectPath } from '../mapping/pathUtils.js';
10
+ import { findSourceField, resolveMappedValue } from '../mapping/resolveMappedValue.js';
11
+ import { findTargetSlot, flattenTargetSlots } from '../mapping/treeUtils.js';
12
+ import type {
13
+ SourceField,
14
+ TargetSlot,
15
+ TransformContext,
16
+ ValueTransformRegistry,
17
+ } from '../types.js';
18
+
19
+ export interface ConvertToShapeInput {
20
+ readonly conversion: ConversionDefinition;
21
+ /** Shape registry, or an explicit list containing at least the referenced shapes. */
22
+ readonly shapes: DataShapeRegistry | readonly DataShape[];
23
+ /**
24
+ * Named input bags keyed by source shape id.
25
+ * Example: `{ order: liveOrder, customer: liveCustomer }`.
26
+ */
27
+ readonly inputs: Readonly<Record<string, unknown>>;
28
+ readonly transforms: ValueTransformRegistry;
29
+ readonly context?: TransformContext;
30
+ }
31
+
32
+ export interface ConvertToShapeSlotResult {
33
+ readonly edgeId: string;
34
+ readonly targetSlotId: string;
35
+ readonly path: string;
36
+ readonly value: unknown;
37
+ }
38
+
39
+ export interface ConvertToShapeResult {
40
+ /** Nested JSON matching target shape paths. */
41
+ readonly output: Record<string, unknown>;
42
+ /** Flat per-slot results (debug / UI panels). */
43
+ readonly slots: readonly ConvertToShapeSlotResult[];
44
+ }
45
+
46
+ function isDataShapeRegistry(
47
+ shapes: DataShapeRegistry | readonly DataShape[],
48
+ ): shapes is DataShapeRegistry {
49
+ return typeof (shapes as DataShapeRegistry).get === 'function';
50
+ }
51
+
52
+ function resolveShape(
53
+ shapes: DataShapeRegistry | readonly DataShape[],
54
+ id: string,
55
+ ): DataShape | undefined {
56
+ if (isDataShapeRegistry(shapes)) {
57
+ return shapes.get(id);
58
+ }
59
+ return shapes.find((shape) => shape.id === id);
60
+ }
61
+
62
+ function outputPathForTarget(slot: TargetSlot): string {
63
+ const path = slot.path?.trim();
64
+ if (path) {
65
+ return path;
66
+ }
67
+ // Prefer last id segment when hosts use dotted ids (`slot.display.timeText`).
68
+ const id = slot.id.trim();
69
+ if (id.includes('.')) {
70
+ const parts = id.split('.').filter(Boolean);
71
+ // Drop a leading registry prefix like `slot` when present with 3+ segments.
72
+ if (parts.length >= 3 && (parts[0] === 'slot' || parts[0] === 'tgt')) {
73
+ return parts.slice(1).join('.');
74
+ }
75
+ if (parts.length >= 2) {
76
+ return parts.slice(1).join('.');
77
+ }
78
+ }
79
+ return slot.label.trim() || id;
80
+ }
81
+
82
+ function readFieldValue(field: SourceField, inputs: Readonly<Record<string, unknown>>): unknown {
83
+ const shapeId = field.shapeId?.trim();
84
+ const bag =
85
+ shapeId && Object.prototype.hasOwnProperty.call(inputs, shapeId)
86
+ ? inputs[shapeId]
87
+ : // Single-bag fallback: use the only input, or the whole inputs record.
88
+ Object.keys(inputs).length === 1
89
+ ? inputs[Object.keys(inputs)[0]!]
90
+ : inputs;
91
+
92
+ const path = field.path?.trim();
93
+ if (path) {
94
+ // When bags are per-shape, paths are relative to that bag.
95
+ // When falling back to the whole inputs record, absolute paths still work.
96
+ if (shapeId && Object.prototype.hasOwnProperty.call(inputs, shapeId)) {
97
+ return readObjectPath(bag, path);
98
+ }
99
+ const fromBag = readObjectPath(bag, path);
100
+ if (fromBag !== undefined) {
101
+ return fromBag;
102
+ }
103
+ // Absolute path into combined inputs (e.g. `order.totalCents`).
104
+ return readObjectPath(inputs, path);
105
+ }
106
+
107
+ return field.sampleValue;
108
+ }
109
+
110
+ /**
111
+ * Apply a conversion to named input bags and build target-shaped JSON.
112
+ *
113
+ * This is the host runtime entry point — not `sourceShape.convert(target, data)`.
114
+ * Multiple source shapes are supported via `inputs[shapeId]`.
115
+ */
116
+ export function convertToShape(input: ConvertToShapeInput): ConvertToShapeResult {
117
+ const sourceShapes: DataShape[] = [];
118
+ for (const shapeId of input.conversion.sourceShapeIds) {
119
+ const shape = resolveShape(input.shapes, shapeId);
120
+ if (!shape) {
121
+ throw new Error(`Unknown source shape "${shapeId}" for conversion "${input.conversion.id}".`);
122
+ }
123
+ if (shape.role === 'target') {
124
+ throw new Error(`Shape "${shapeId}" has role "target" and cannot be used as a source.`);
125
+ }
126
+ sourceShapes.push(shape);
127
+ }
128
+
129
+ const targetShape = resolveShape(input.shapes, input.conversion.targetShapeId);
130
+ if (!targetShape) {
131
+ throw new Error(
132
+ `Unknown target shape "${input.conversion.targetShapeId}" for conversion "${input.conversion.id}".`,
133
+ );
134
+ }
135
+
136
+ const sources = mergeSourceShapes(sourceShapes);
137
+ const targets = targetSlotsFromShape(targetShape);
138
+ const targetLeaves = flattenTargetSlots(targets);
139
+
140
+ let output: Record<string, unknown> = {};
141
+ const slots: ConvertToShapeSlotResult[] = [];
142
+
143
+ for (const edge of input.conversion.document.edges) {
144
+ const sourceField = findSourceField(sources, edge.sourceFieldId);
145
+ if (!sourceField) {
146
+ continue;
147
+ }
148
+ const targetSlot =
149
+ findTargetSlot(targets, edge.targetSlotId) ??
150
+ targetLeaves.find((slot) => slot.id === edge.targetSlotId);
151
+ if (!targetSlot) {
152
+ continue;
153
+ }
154
+
155
+ const sourceValue = readFieldValue(sourceField, input.inputs);
156
+ const value =
157
+ edge.itemEdges && edge.itemEdges.length > 0
158
+ ? convertArrayWithItemEdges({
159
+ items: sourceValue,
160
+ itemEdges: edge.itemEdges,
161
+ sources,
162
+ targets,
163
+ transforms: input.transforms,
164
+ context: input.context,
165
+ })
166
+ : resolveMappedValue(edge, sourceValue, input.transforms, {
167
+ ...input.context,
168
+ sampleValue: sourceValue,
169
+ record: isPlainObject(sourceValue)
170
+ ? sourceValue
171
+ : isPlainObject(input.context?.record)
172
+ ? input.context.record
173
+ : undefined,
174
+ });
175
+
176
+ const path = outputPathForTarget(targetSlot);
177
+ output = writeObjectPath(output, path, value);
178
+ slots.push({
179
+ edgeId: edge.id,
180
+ targetSlotId: edge.targetSlotId,
181
+ path,
182
+ value,
183
+ });
184
+ }
185
+
186
+ return { output, slots };
187
+ }
@@ -1,109 +1,109 @@
1
- import type { SourceField, TargetSlot } from '../types.js';
2
-
3
- /** How a managed shape is used in conversions. */
4
- export type DataShapeRole = 'source' | 'target' | 'both';
5
-
6
- /**
7
- * Managed structure descriptor ("class-like" fixed shape).
8
- * Source/target field trees are host-owned; this wraps them for registry lookup.
9
- */
10
- export interface DataShape {
11
- readonly id: string;
12
- readonly label: string;
13
- readonly role: DataShapeRole;
14
- /**
15
- * Field tree for source and/or target use.
16
- * When `role` is `target`, treat as `TargetSlot[]` (compatible structural shape).
17
- * When `role` is `source` or `both`, treat as `SourceField[]`.
18
- */
19
- readonly fields: readonly SourceField[] | readonly TargetSlot[];
20
- }
21
-
22
- export interface DataShapeRegistry {
23
- register(shape: DataShape): void;
24
- get(id: string): DataShape | undefined;
25
- list(role?: DataShapeRole): readonly DataShape[];
26
- }
27
-
28
- export function defineDataShape(input: DataShape): DataShape {
29
- const id = input.id.trim();
30
- if (!id) {
31
- throw new Error('DataShape.id must be a non-empty string.');
32
- }
33
- return {
34
- id,
35
- label: input.label.trim() || id,
36
- role: input.role,
37
- fields: input.fields,
38
- };
39
- }
40
-
41
- export function createDataShapeRegistry(initial: readonly DataShape[] = []): DataShapeRegistry {
42
- const byId = new Map<string, DataShape>();
43
-
44
- const api: DataShapeRegistry = {
45
- register(shape) {
46
- const defined = defineDataShape(shape);
47
- byId.set(defined.id, defined);
48
- },
49
- get(id) {
50
- return byId.get(id.trim());
51
- },
52
- list(role) {
53
- const all = [...byId.values()];
54
- if (!role) {
55
- return all;
56
- }
57
- return all.filter((shape) => shape.role === role || shape.role === 'both');
58
- },
59
- };
60
-
61
- for (const shape of initial) {
62
- api.register(shape);
63
- }
64
- return api;
65
- }
66
-
67
- /** Recursively stamp `shapeId` on source fields (for multi-input convert). */
68
- export function attachShapeIdToSourceFields(
69
- fields: readonly SourceField[],
70
- shapeId: string,
71
- ): SourceField[] {
72
- return fields.map((field) => {
73
- const next: SourceField = {
74
- ...field,
75
- shapeId,
76
- ...(field.children ? { children: attachShapeIdToSourceFields(field.children, shapeId) } : {}),
77
- };
78
- return next;
79
- });
80
- }
81
-
82
- /**
83
- * Merge source shapes into one tree for `FieldRemap` / convert:
84
- * each shape becomes a non-mappable group whose children carry `shapeId`.
85
- */
86
- export function mergeSourceShapes(shapes: readonly DataShape[]): SourceField[] {
87
- const sources: SourceField[] = [];
88
- for (const shape of shapes) {
89
- if (shape.role === 'target') {
90
- continue;
91
- }
92
- const children = attachShapeIdToSourceFields(shape.fields as readonly SourceField[], shape.id);
93
- sources.push({
94
- id: `shape.${shape.id}`,
95
- label: shape.label,
96
- group: 'Shapes',
97
- children,
98
- });
99
- }
100
- return sources;
101
- }
102
-
103
- /** Target slots from a target (or both) shape. */
104
- export function targetSlotsFromShape(shape: DataShape): TargetSlot[] {
105
- if (shape.role === 'source') {
106
- throw new Error(`DataShape "${shape.id}" has role "source" and cannot provide target slots.`);
107
- }
108
- return shape.fields as TargetSlot[];
109
- }
1
+ import type { SourceField, TargetSlot } from '../types.js';
2
+
3
+ /** How a managed shape is used in conversions. */
4
+ export type DataShapeRole = 'source' | 'target' | 'both';
5
+
6
+ /**
7
+ * Managed structure descriptor ("class-like" fixed shape).
8
+ * Source/target field trees are host-owned; this wraps them for registry lookup.
9
+ */
10
+ export interface DataShape {
11
+ readonly id: string;
12
+ readonly label: string;
13
+ readonly role: DataShapeRole;
14
+ /**
15
+ * Field tree for source and/or target use.
16
+ * When `role` is `target`, treat as `TargetSlot[]` (compatible structural shape).
17
+ * When `role` is `source` or `both`, treat as `SourceField[]`.
18
+ */
19
+ readonly fields: readonly SourceField[] | readonly TargetSlot[];
20
+ }
21
+
22
+ export interface DataShapeRegistry {
23
+ register(shape: DataShape): void;
24
+ get(id: string): DataShape | undefined;
25
+ list(role?: DataShapeRole): readonly DataShape[];
26
+ }
27
+
28
+ export function defineDataShape(input: DataShape): DataShape {
29
+ const id = input.id.trim();
30
+ if (!id) {
31
+ throw new Error('DataShape.id must be a non-empty string.');
32
+ }
33
+ return {
34
+ id,
35
+ label: input.label.trim() || id,
36
+ role: input.role,
37
+ fields: input.fields,
38
+ };
39
+ }
40
+
41
+ export function createDataShapeRegistry(initial: readonly DataShape[] = []): DataShapeRegistry {
42
+ const byId = new Map<string, DataShape>();
43
+
44
+ const api: DataShapeRegistry = {
45
+ register(shape) {
46
+ const defined = defineDataShape(shape);
47
+ byId.set(defined.id, defined);
48
+ },
49
+ get(id) {
50
+ return byId.get(id.trim());
51
+ },
52
+ list(role) {
53
+ const all = [...byId.values()];
54
+ if (!role) {
55
+ return all;
56
+ }
57
+ return all.filter((shape) => shape.role === role || shape.role === 'both');
58
+ },
59
+ };
60
+
61
+ for (const shape of initial) {
62
+ api.register(shape);
63
+ }
64
+ return api;
65
+ }
66
+
67
+ /** Recursively stamp `shapeId` on source fields (for multi-input convert). */
68
+ export function attachShapeIdToSourceFields(
69
+ fields: readonly SourceField[],
70
+ shapeId: string,
71
+ ): SourceField[] {
72
+ return fields.map((field) => {
73
+ const next: SourceField = {
74
+ ...field,
75
+ shapeId,
76
+ ...(field.children ? { children: attachShapeIdToSourceFields(field.children, shapeId) } : {}),
77
+ };
78
+ return next;
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Merge source shapes into one tree for `FieldRemap` / convert:
84
+ * each shape becomes a non-mappable group whose children carry `shapeId`.
85
+ */
86
+ export function mergeSourceShapes(shapes: readonly DataShape[]): SourceField[] {
87
+ const sources: SourceField[] = [];
88
+ for (const shape of shapes) {
89
+ if (shape.role === 'target') {
90
+ continue;
91
+ }
92
+ const children = attachShapeIdToSourceFields(shape.fields as readonly SourceField[], shape.id);
93
+ sources.push({
94
+ id: `shape.${shape.id}`,
95
+ label: shape.label,
96
+ group: 'Shapes',
97
+ children,
98
+ });
99
+ }
100
+ return sources;
101
+ }
102
+
103
+ /** Target slots from a target (or both) shape. */
104
+ export function targetSlotsFromShape(shape: DataShape): TargetSlot[] {
105
+ if (shape.role === 'source') {
106
+ throw new Error(`DataShape "${shape.id}" has role "source" and cannot provide target slots.`);
107
+ }
108
+ return shape.fields as TargetSlot[];
109
+ }