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

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/README.md CHANGED
@@ -1,115 +1,288 @@
1
- # `@workbench-kit/field-remap`
2
-
3
- Field remap **runtime**: reshape structure A into structure B with mapping edges and `convertToShape`.
4
-
5
- This package does **not** ship a mapping UI. Hosts adapt a tree or table UI into `MappingEdge[]`
6
- and call `convertToShape`. The workbench sample (**Field Remap → A → B**) demonstrates a nested
7
- tree mapper with list context; flat OSS adapters (for example `react-table-mapping`) remain useful
8
- for leaf-only hosts.
9
-
10
- ## Install
11
-
12
- ```powershell
13
- pnpm add @workbench-kit/field-remap@prototype
14
- ```
15
-
16
- ## Capabilities
17
-
18
- | Pattern | Support |
19
- | ----------------------------------------- | -------------------------------------------------------------------- |
20
- | Leaf → leaf rename | Yes |
21
- | Nested object paths | Yes (`path` + `writeObjectPath`) |
22
- | Array whole copy | Yes (`identity`) |
23
- | Array item projection | Yes (`itemSourcePath`) |
24
- | Array → scalar reduce | Yes (`array:first`, `array:join`) |
25
- | String format chain | Yes (`string:trim` / `upper` / `lower` / `prefix` / `suffix`, max 3) |
26
- | Array<object> → Array<object> | Yes (`itemEdges` list context) |
27
- | Index / wildcard paths | No (P2) |
28
-
29
- Middle “graph nodes” in the sample UI are just `MappingEdge.transformIds` steps
30
- (plus optional `transformOptionSteps`), not a separate document type. The workbench
31
- sample renders them with `@xyflow/react` (source out transform target in).
32
-
33
- ## Quick start
34
-
35
- ```ts
36
- import {
37
- convertToShape,
38
- createBuiltinValueTransformRegistry,
39
- defineConversion,
40
- defineDataShape,
41
- sourceFieldsFromPlainObject,
42
- targetSlotsFromPlainObject,
43
- } from '@workbench-kit/field-remap';
44
-
45
- const structureA = {
46
- user_name: 'Ada',
47
- tags: [{ name: 'math' }, { name: 'computing' }],
48
- };
49
-
50
- const shapes = [
51
- defineDataShape({
52
- id: 'a',
53
- label: 'A',
54
- role: 'source',
55
- fields: sourceFieldsFromPlainObject(structureA, { idPrefix: 'a' }),
56
- }),
57
- defineDataShape({
58
- id: 'b',
59
- label: 'B',
60
- role: 'target',
61
- fields: targetSlotsFromPlainObject({ name: '', labels: [{ title: '' }] }, { idPrefix: 'b' }),
62
- }),
63
- ];
64
-
65
- const conversion = defineConversion({
66
- id: 'a→b',
67
- sourceShapeIds: ['a'],
68
- targetShapeId: 'b',
69
- edges: [
70
- {
71
- id: 'e-name',
72
- sourceFieldId: 'a.user_name',
73
- targetSlotId: 'b.name',
74
- },
75
- {
76
- id: 'e-tags',
77
- sourceFieldId: 'a.tags',
78
- targetSlotId: 'b.labels',
79
- itemEdges: [
80
- {
81
- id: 'e-title',
82
- sourceFieldId: 'a.tags.item.name',
83
- targetSlotId: 'b.labels.item.title',
84
- },
85
- ],
86
- },
87
- ],
88
- });
89
-
90
- const { output } = convertToShape({
91
- conversion,
92
- shapes,
93
- inputs: { a: structureA },
94
- transforms: createBuiltinValueTransformRegistry(),
95
- });
96
- // { name: 'Ada', labels: [{ title: 'math' }, { title: 'computing' }] }
97
- ```
98
-
99
- Hosts may `registry.register()` additional transforms (the sample registers `expr:jsonata` via
100
- [jsonata](https://jsonata.org/)).
101
-
102
- ## Layout
103
-
104
- ```text
105
- src/
106
- domain/document/ edges + FieldRemapDocument
107
- domain/shapes/ DataShape, ConversionDefinition, convertToShape
108
- domain/ingest/ plain object → fields / slots
109
- domain/mapping/ path helpers, list context, conflicts
110
- registry/ ValueTransform registry (identity, array:first, array:join)
111
- ```
112
-
113
- ## Stability
114
-
115
- Published on the npm `@prototype` tag. Prefer the root export; deep paths are unsupported.
1
+ # `@workbench-kit/field-remap`
2
+
3
+ Field remap **runtime**: reshape structure A into structure B with mapping edges and `convertToShape`.
4
+
5
+ ## Interaction model (host UI)
6
+
7
+ The intended mapper mental model matching the shell Flow sample is:
8
+
9
+ 1. **Source schema (A)** and **target schema (B)** as multi-port columns (fields with types /
10
+ nested paths). Hosts own these shapes; they are not stored in `FieldRemapDocument`.
11
+ 2. **Optional convert steps** in the middle (`string:trim`, `string:upper`, `array:first`,
12
+ `array:join`, …) when a binding needs transforms.
13
+ 3. **Port-to-port wires (DnD)** from source → [converters] → target. Each wire is a
14
+ `MappingEdge` (`transformIds` = convert chain). There is no free-form graph document.
15
+
16
+ This package does **not** ship a mapping UI. Hosts adapt a Flow / tree / table UI into
17
+ `MappingEdge[]` and call `convertToShape`. The workbench sample (**Field Remap → A → B**)
18
+ demonstrates the schema-column + convert-wire topology with list context; flat OSS adapters
19
+ (for example `react-table-mapping`) remain useful for leaf-only hosts.
20
+
21
+ ## Install
22
+
23
+ ```powershell
24
+ pnpm add @workbench-kit/field-remap@prototype
25
+ ```
26
+
27
+ ## Capabilities
28
+
29
+ | Pattern | Support |
30
+ | ----------------------------------------- | -------------------------------------------------------------------- |
31
+ | Leafleaf rename | Yes |
32
+ | Nested object paths | Yes (`path` + `writeObjectPath`) |
33
+ | Array whole copy | Yes (`identity`) |
34
+ | Array item projection | Yes (`itemSourcePath`) |
35
+ | Array → scalar reduce | Yes (`array:first`, `array:join`) |
36
+ | String format chain | Yes (`string:trim` / `upper` / `lower` / `prefix` / `suffix`, max 3) |
37
+ | Array<object> → Array<object> | Yes (`itemEdges` list context) |
38
+ | Index / wildcard paths | Yes (`items[0].name`, `items[*].name` via `projectObjectPath`) |
39
+ | n→m combine / split operators | Yes (`applyMappingOperators`; document v2 `operators[]`) |
40
+
41
+ ### Path grammar
42
+
43
+ Safe object paths are dotted identifiers with optional index / wildcard brackets:
44
+
45
+ | Form | Example | API |
46
+ | -------- | --------------- | -------------------------------------------------------- |
47
+ | Property | `meta.label` | `readObjectPath` / `writeObjectPath` |
48
+ | Index | `items[0].name` | `readObjectPath` / `writeObjectPath` |
49
+ | Wildcard | `items[*].name` | `projectObjectPath` only (`readObjectPath` fails closed) |
50
+
51
+ Wildcard expansion is capped by `DEFAULT_MAX_PATH_WILDCARD_EXPANSION` (1000) or
52
+ `projectObjectPath(..., { maxExpansion })`. This is not a JSONPath engine.
53
+
54
+ Middle convert nodes in the sample UI are just `MappingEdge.transformIds` steps
55
+ (plus optional `transformOptionSteps`), not a separate document type. The workbench
56
+ sample renders them with `@xyflow/react` (source schema → convert → target schema).
57
+ Selecting a convert note opens a dedicated **Convert note editor** side surface
58
+ (`ConvertNoteEditor` in `@workbench-kit/shell-react`); binding/edge selection keeps
59
+ a lighter mapping detail rail (chain overview, palette, list context).
60
+
61
+ ### Shape ownership
62
+
63
+ `FieldRemapDocument` (v1) stores **edges only**. Hosts own input/output shapes
64
+ (`SourceField[]` / `TargetSlot[]`, or `defineDataShape` + ingest helpers) and pass
65
+ them into `convertToShape` / the shell `FieldRemapPanel` / `FieldRemapFlowMapper`.
66
+ Optional `classRef` / `hidden` on fields and slots are additive; use
67
+ `projectShapes` / `projectSourceFields` / `projectTargetSlots` with
68
+ `includeHidden` (default omit hidden) before wiring Flow columns, and
69
+ `pruneMappingEdgesForShapes` after ingest when ids disappear. The shell panel’s
70
+ shape IO editor (paste JSON → ingest + `FieldDataType` selects) is an in-memory
71
+ host aid; browse-first hosts can set `ioChrome="browse"` /
72
+ `FieldRemapIoClassBrowse` instead. Neither path extends the persisted document.
73
+
74
+ ### Host embed (shell UI)
75
+
76
+ Published packages already include the runtime and host-embeddable UI (no monorepo
77
+ checkout required once your pin includes a release that contains these exports):
78
+
79
+ ```powershell
80
+ pnpm add @workbench-kit/field-remap@prototype @workbench-kit/shell-react@prototype
81
+ ```
82
+
83
+ ```ts
84
+ import {
85
+ convertMappedInputs,
86
+ convertToShape,
87
+ createBuiltinValueTransformRegistry,
88
+ defineConversion,
89
+ defineDataShape,
90
+ sourceFieldsFromPlainObject,
91
+ targetSlotsFromPlainObject,
92
+ } from '@workbench-kit/field-remap';
93
+ import {
94
+ FieldRemapFlowMapper,
95
+ FieldRemapPanel,
96
+ createJsonataValueTransform,
97
+ } from '@workbench-kit/shell-react/field-remap';
98
+ import '@workbench-kit/shell-react/field-remap/view.css';
99
+
100
+ // Quick demo surface (catalog sample + preview):
101
+ // <FieldRemapPanel sample="nested-ab" />
102
+
103
+ // Controlled panel (host persists edges):
104
+ // <FieldRemapPanel edges={edges} onEdgesChange={setEdges} sources={…} targets={…} sourceSample={…} />
105
+
106
+ // Or host-owned shapes + Flow-only embed:
107
+ const transforms = createBuiltinValueTransformRegistry();
108
+ transforms.register(createJsonataValueTransform());
109
+ // <FieldRemapFlowMapper sources={…} targets={…} edges={…} transforms={transforms} onEdgesChange={…} />
110
+
111
+ // Evaluate transform-bearing edges without a FieldRemapDocument:
112
+ // await convertMappedInputs({ sources, targets, edges, inputs: { source: bag }, transforms })
113
+ ```
114
+
115
+ Prefer `convertMappedInputs` when the host catalog stores `MappingEdge[]` (+ optional
116
+ `operators[]`) separately from kit document JSON. Prefer `convertToShape` when you already
117
+ build `defineConversion` / `defineDataShape` registries yourself.
118
+
119
+ Place-then-wire uses **ephemeral draft nodes** in the shell Flow UI: place a
120
+ transform, wire source then target (or the reverse), and the draft finalizes into
121
+ a `MappingEdge` with `transformIds: [id]`. Escape discards unfinished drafts.
122
+ The persisted document stays edges-only — no free graph. You can also add steps
123
+ via the detail palette / `+ node` onto an existing binding (max 3). List context
124
+ uses `itemEdges` on array→array bindings.
125
+
126
+ **Persisted `xf:*` connect matrix** (shell Flow adapter; no silent no-ops):
127
+
128
+ | Drag | Effect |
129
+ | ---------------------------- | ---------------------------------------------------------------- |
130
+ | source port → target port | upsert `MappingEdge` |
131
+ | source port → `xf:edge:step` | rebind source; keep transforms from that step (splice prefix) |
132
+ | `xf:edge:step` → target port | rebind target; keep transforms through that step (splice suffix) |
133
+ | `xf:A:i` → `xf:B:j` (A≠B) | merge chains (append A prefix + B suffix); remove donor edge A |
134
+ | same-edge `xf`↔`xf` | rejected (mid segments already exist) |
135
+
136
+ ### n→m operators (combine / split)
137
+
138
+ `FieldRemapDocument` **v1** is edges-only (1→1 bindings). **v2** (current) adds an
139
+ optional `operators[]` list for fan-in / fan-out. Call `applyMappingOperators` with
140
+ `combine` / `split` operators (limits: `MAX_MAPPING_FAN_IN` / `MAX_MAPPING_FAN_OUT`
141
+ = 8). Hosts may merge the result with `convertToShape` output.
142
+ `migrateFieldRemapDocument` / `parseFieldRemapDocument` accept v1 and v2 and always
143
+ emit the current version. Shell Flow renders combine/split as multi-port nodes and
144
+ supports authoring (create / wire ports / delete) when hosts pass `operators` +
145
+ `onOperatorsChange` into `FieldRemapFlowMapper` (sample `nm-combine-split`).
146
+
147
+ ```ts
148
+ import {
149
+ applyMappingOperators,
150
+ createBuiltinValueTransformRegistry,
151
+ } from '@workbench-kit/field-remap';
152
+
153
+ const { output } = await applyMappingOperators({
154
+ operators: [
155
+ {
156
+ kind: 'combine',
157
+ id: 'c1',
158
+ inputFieldIds: ['a.date', 'a.time'],
159
+ outputSlotId: 'b.startsAt',
160
+ transformIds: ['datetime:combine'],
161
+ },
162
+ ],
163
+ sources,
164
+ targets,
165
+ inputs: { a: { date: '2026-07-20', time: '14:30:00' } },
166
+ transforms: createBuiltinValueTransformRegistry(),
167
+ });
168
+ ```
169
+
170
+ ### Port compatibility
171
+
172
+ Use `areFieldTypesCompatible` for identity (direct) links and `arePortsCompatible` when a
173
+ `transformIds` chain may mediate the link. Empty / omitted chains are identity matches;
174
+ non-empty chains require a `ValueTransformRegistry` and reuse `isTransformChainCompatible`.
175
+ Missing or `unknown` `FieldDataType` values stay permissive (same default as transform helpers).
176
+
177
+ ```ts
178
+ import {
179
+ areFieldTypesCompatible,
180
+ arePortsCompatible,
181
+ createBuiltinValueTransformRegistry,
182
+ } from '@workbench-kit/field-remap';
183
+
184
+ areFieldTypesCompatible('string', 'string'); // true
185
+ areFieldTypesCompatible('string', 'number'); // false
186
+
187
+ const transforms = createBuiltinValueTransformRegistry();
188
+ arePortsCompatible({
189
+ sourceType: 'array',
190
+ targetType: 'string',
191
+ transformIds: ['array:join'],
192
+ registry: transforms,
193
+ }); // true
194
+ ```
195
+
196
+ ## Quick start
197
+
198
+ ```ts
199
+ import {
200
+ convertToShape,
201
+ createBuiltinValueTransformRegistry,
202
+ defineConversion,
203
+ defineDataShape,
204
+ sourceFieldsFromPlainObject,
205
+ targetSlotsFromPlainObject,
206
+ } from '@workbench-kit/field-remap';
207
+
208
+ const structureA = {
209
+ user_name: 'Ada',
210
+ tags: [{ name: 'math' }, { name: 'computing' }],
211
+ };
212
+
213
+ const shapes = [
214
+ defineDataShape({
215
+ id: 'a',
216
+ label: 'A',
217
+ role: 'source',
218
+ fields: sourceFieldsFromPlainObject(structureA, { idPrefix: 'a' }),
219
+ }),
220
+ defineDataShape({
221
+ id: 'b',
222
+ label: 'B',
223
+ role: 'target',
224
+ fields: targetSlotsFromPlainObject({ name: '', labels: [{ title: '' }] }, { idPrefix: 'b' }),
225
+ }),
226
+ ];
227
+
228
+ const conversion = defineConversion({
229
+ id: 'a→b',
230
+ sourceShapeIds: ['a'],
231
+ targetShapeId: 'b',
232
+ edges: [
233
+ {
234
+ id: 'e-name',
235
+ sourceFieldId: 'a.user_name',
236
+ targetSlotId: 'b.name',
237
+ },
238
+ {
239
+ id: 'e-tags',
240
+ sourceFieldId: 'a.tags',
241
+ targetSlotId: 'b.labels',
242
+ itemEdges: [
243
+ {
244
+ id: 'e-title',
245
+ sourceFieldId: 'a.tags.item.name',
246
+ targetSlotId: 'b.labels.item.title',
247
+ },
248
+ ],
249
+ },
250
+ ],
251
+ });
252
+
253
+ const { output } = await convertToShape({
254
+ conversion,
255
+ shapes,
256
+ inputs: { a: structureA },
257
+ transforms: createBuiltinValueTransformRegistry(),
258
+ });
259
+ // { name: 'Ada', labels: [{ title: 'math' }, { title: 'computing' }] }
260
+ ```
261
+
262
+ Hosts may `registry.register()` additional transforms (the sample registers `expr:jsonata` via
263
+ [jsonata](https://jsonata.org/)). `convertToShape` / `applyTransformChain` are async so Promise-returning
264
+ host transforms (JSONata 2.x) resolve correctly.
265
+
266
+ ### Cancellation
267
+
268
+ Pass `signal` on `convertToShape` (or `TransformContext.signal`) to cancel stale previews.
269
+ Aborted runs reject with `AbortError` and stop further edges / chain steps. The shell Field Remap
270
+ panel wires an `AbortController` to effect cleanup.
271
+
272
+ Host JSONata transforms in `@workbench-kit/shell-react` are bounded by default (`timeoutMs`,
273
+ `maxExpressionLength`, `onError: 'throw'`). Use `createJsonataValueTransform()` to override.
274
+
275
+ ## Layout
276
+
277
+ ```text
278
+ src/
279
+ domain/document/ edges + FieldRemapDocument
280
+ domain/shapes/ DataShape, ConversionDefinition, convertToShape
281
+ domain/ingest/ plain object → fields / slots
282
+ domain/mapping/ path helpers, list context, conflicts
283
+ registry/ ValueTransform registry (identity, array:first, array:join)
284
+ ```
285
+
286
+ ## Stability
287
+
288
+ Published on the npm `@prototype` tag. Prefer the root export; deep paths are unsupported.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-kit/field-remap",
3
- "version": "0.0.1-prototype.0",
3
+ "version": "0.0.2-prototype.0.2.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,29 @@
1
+ /** Create an Error with `name === 'AbortError'` (DOMException when available). */
2
+ export function createAbortError(message = 'The operation was aborted.'): Error {
3
+ if (typeof DOMException !== 'undefined') {
4
+ return new DOMException(message, 'AbortError');
5
+ }
6
+ const error = new Error(message);
7
+ error.name = 'AbortError';
8
+ return error;
9
+ }
10
+
11
+ export function isAbortError(error: unknown): boolean {
12
+ return (
13
+ (typeof DOMException !== 'undefined' &&
14
+ error instanceof DOMException &&
15
+ error.name === 'AbortError') ||
16
+ (error instanceof Error && error.name === 'AbortError')
17
+ );
18
+ }
19
+
20
+ /** Throw when `signal` is already aborted. */
21
+ export function throwIfAborted(signal?: AbortSignal): void {
22
+ if (!signal?.aborted) {
23
+ return;
24
+ }
25
+ if (signal.reason instanceof Error) {
26
+ throw signal.reason;
27
+ }
28
+ throw createAbortError();
29
+ }
@@ -1,14 +1,14 @@
1
- /** Maximum ordered transforms applied on a single edge. */
2
- export const MAX_TRANSFORM_CHAIN = 3;
3
-
4
- /** Built-in pass-through transform id (kept here to avoid import cycles). */
5
- export const IDENTITY_TRANSFORM_ID = 'identity';
6
-
7
- /** Legacy id aliases → canonical ids (empty until hosts need migration). */
8
- export const TRANSFORM_ID_ALIASES: Readonly<Record<string, string>> = {};
9
-
10
- /** Map a legacy or canonical transform id to its canonical form. */
11
- export function canonicalizeTransformId(id: string): string {
12
- const trimmed = id.trim();
13
- return TRANSFORM_ID_ALIASES[trimmed] ?? trimmed;
14
- }
1
+ /** Maximum ordered transforms applied on a single edge. */
2
+ export const MAX_TRANSFORM_CHAIN = 3;
3
+
4
+ /** Built-in pass-through transform id (kept here to avoid import cycles). */
5
+ export const IDENTITY_TRANSFORM_ID = 'identity';
6
+
7
+ /** Legacy id aliases → canonical ids (empty until hosts need migration). */
8
+ export const TRANSFORM_ID_ALIASES: Readonly<Record<string, string>> = {};
9
+
10
+ /** Map a legacy or canonical transform id to its canonical form. */
11
+ export function canonicalizeTransformId(id: string): string {
12
+ const trimmed = id.trim();
13
+ return TRANSFORM_ID_ALIASES[trimmed] ?? trimmed;
14
+ }