@workbench-kit/field-remap 0.0.1-prototype.0 → 0.0.2-prototype.0.2.10
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 +288 -115
- package/package.json +1 -1
- package/src/domain/abort.ts +29 -0
- package/src/domain/constants.ts +14 -14
- package/src/domain/document/fieldRemapDocument.ts +163 -129
- package/src/domain/document/mappingEdge.ts +136 -136
- package/src/domain/ingest/sourceFieldsFromPlainObject.ts +103 -103
- package/src/domain/ingest/targetSlotsFromPlainObject.ts +41 -41
- package/src/domain/mapping/convertItemEdges.ts +98 -93
- package/src/domain/mapping/dateFormat.ts +102 -102
- package/src/domain/mapping/mappingConflicts.ts +90 -90
- package/src/domain/mapping/mappingOperators.ts +297 -0
- package/src/domain/mapping/objectPathSafety.ts +141 -0
- package/src/domain/mapping/pathUtils.ts +302 -156
- package/src/domain/mapping/resolveMappedValue.ts +98 -96
- package/src/domain/mapping/transformOptions.ts +184 -184
- package/src/domain/mapping/treeUtils.ts +32 -32
- package/src/domain/shapes/conversionDefinition.ts +95 -95
- package/src/domain/shapes/convertMappedInputs.ts +158 -0
- package/src/domain/shapes/convertToShape.ts +205 -187
- package/src/domain/shapes/dataShape.ts +109 -109
- package/src/domain/shapes/projectShapes.ts +116 -0
- package/src/domain/shapes/shapeEdit.ts +114 -0
- package/src/domain/types.ts +242 -182
- package/src/index.ts +197 -137
- package/src/registry/builtinTransforms.ts +243 -243
- package/src/registry/createValueTransformRegistry.ts +235 -194
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
| Leaf → leaf 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
|
@@ -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
|
+
}
|
package/src/domain/constants.ts
CHANGED
|
@@ -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
|
+
}
|