@workbench-kit/field-remap 0.0.2-prototype.0.2.26 → 0.0.2-prototype.0.2.28
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 +12 -11
- package/package.json +1 -1
- package/src/domain/constants.ts +0 -9
- package/src/domain/document/fieldRemapDocument.ts +7 -47
- package/src/domain/document/mappingEdge.ts +6 -52
- package/src/domain/mapping/convertItemEdges.ts +1 -1
- package/src/domain/mapping/mappingOperators.ts +2 -6
- package/src/domain/mapping/objectPathSafety.ts +0 -15
- package/src/domain/mapping/resolveMappedValue.ts +4 -9
- package/src/domain/mapping/transformOptions.ts +3 -112
- package/src/domain/types.ts +6 -27
- package/src/index.ts +1 -14
- package/src/registry/createValueTransformRegistry.ts +2 -3
package/README.md
CHANGED
|
@@ -60,9 +60,10 @@ a lighter mapping detail rail (chain overview, palette, list context).
|
|
|
60
60
|
|
|
61
61
|
### Shape ownership
|
|
62
62
|
|
|
63
|
-
`FieldRemapDocument`
|
|
64
|
-
(`SourceField[]` / `TargetSlot[]`, or `defineDataShape` + ingest
|
|
65
|
-
them into `convertToShape` / the shell `FieldRemapPanel` /
|
|
63
|
+
`FieldRemapDocument` stores edges plus optional combine/split operators. Hosts own
|
|
64
|
+
input/output shapes (`SourceField[]` / `TargetSlot[]`, or `defineDataShape` + ingest
|
|
65
|
+
helpers) and pass them into `convertToShape` / the shell `FieldRemapPanel` /
|
|
66
|
+
`FieldRemapFlowMapper`.
|
|
66
67
|
Optional `classRef` / `hidden` on fields and slots are additive; use
|
|
67
68
|
`projectShapes` / `projectSourceFields` / `projectTargetSlots` with
|
|
68
69
|
`includeHidden` (default omit hidden) before wiring Flow columns, and
|
|
@@ -119,7 +120,7 @@ build `defineConversion` / `defineDataShape` registries yourself.
|
|
|
119
120
|
Place-then-wire uses **ephemeral draft nodes** in the shell Flow UI: place a
|
|
120
121
|
transform, wire source then target (or the reverse), and the draft finalizes into
|
|
121
122
|
a `MappingEdge` with `transformIds: [id]`. Escape discards unfinished drafts.
|
|
122
|
-
The persisted document stays
|
|
123
|
+
The persisted document stays mapping-only — no free graph nodes. You can also add steps
|
|
123
124
|
via the detail palette / `+ node` onto an existing binding (max 3). List context
|
|
124
125
|
uses `itemEdges` on array→array bindings.
|
|
125
126
|
|
|
@@ -135,13 +136,13 @@ uses `itemEdges` on array→array bindings.
|
|
|
135
136
|
|
|
136
137
|
### n→m operators (combine / split)
|
|
137
138
|
|
|
138
|
-
`FieldRemapDocument`
|
|
139
|
-
|
|
139
|
+
`FieldRemapDocument` v2 includes an optional `operators[]` list for fan-in / fan-out.
|
|
140
|
+
Call `applyMappingOperators` with
|
|
140
141
|
`combine` / `split` operators (limits: `MAX_MAPPING_FAN_IN` / `MAX_MAPPING_FAN_OUT`
|
|
141
142
|
= 8). Hosts may merge the result with `convertToShape` output.
|
|
142
|
-
`
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
`parseFieldRemapDocument` accepts the current version and normalizes edges and operators.
|
|
144
|
+
Shell Flow renders combine/split as multi-port nodes and supports authoring
|
|
145
|
+
(create / wire ports / delete) when hosts pass `operators` +
|
|
145
146
|
`onOperatorsChange` into `FieldRemapFlowMapper` (sample `nm-combine-split`).
|
|
146
147
|
|
|
147
148
|
```ts
|
|
@@ -269,8 +270,8 @@ Pass `signal` on `convertToShape` (or `TransformContext.signal`) to cancel stale
|
|
|
269
270
|
Aborted runs reject with `AbortError` and stop further edges / chain steps. The shell Field Remap
|
|
270
271
|
panel wires an `AbortController` to effect cleanup.
|
|
271
272
|
|
|
272
|
-
Host JSONata transforms in `@workbench-kit/shell-react` are bounded by default
|
|
273
|
-
`
|
|
273
|
+
Host JSONata transforms in `@workbench-kit/shell-react` are fail-closed and bounded by default
|
|
274
|
+
(`timeoutMs`, `maxExpressionLength`). Use `createJsonataValueTransform()` to override the bounds.
|
|
274
275
|
|
|
275
276
|
## Layout
|
|
276
277
|
|
package/package.json
CHANGED
package/src/domain/constants.ts
CHANGED
|
@@ -3,12 +3,3 @@ export const MAX_TRANSFORM_CHAIN = 3;
|
|
|
3
3
|
|
|
4
4
|
/** Built-in pass-through transform id (kept here to avoid import cycles). */
|
|
5
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
|
-
}
|
|
@@ -4,20 +4,15 @@ import type { MappingEdge, MappingOperator, FieldRemapDocument } from '../types.
|
|
|
4
4
|
|
|
5
5
|
/** Current persistence version (edges + optional `operators[]`). */
|
|
6
6
|
export const FIELD_REMAP_DOCUMENT_VERSION = 2 as const;
|
|
7
|
-
/** Legacy edges-only documents. */
|
|
8
|
-
export const FIELD_REMAP_DOCUMENT_V1_VERSION = 1 as const;
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
typeof FIELD_REMAP_DOCUMENT_VERSION | typeof FIELD_REMAP_DOCUMENT_V1_VERSION;
|
|
12
|
-
|
|
13
|
-
/** Thrown when parse/deserialize sees a document `version` other than a supported constant. */
|
|
8
|
+
/** Thrown when parse/deserialize sees a document `version` other than the current constant. */
|
|
14
9
|
export class UnsupportedFieldRemapDocumentVersionError extends Error {
|
|
15
10
|
readonly version: unknown;
|
|
16
11
|
readonly expectedVersion: typeof FIELD_REMAP_DOCUMENT_VERSION;
|
|
17
12
|
|
|
18
13
|
constructor(version: unknown) {
|
|
19
14
|
super(
|
|
20
|
-
`Unsupported field remap document version ${String(version)}; expected ${
|
|
15
|
+
`Unsupported field remap document version ${String(version)}; expected ${FIELD_REMAP_DOCUMENT_VERSION}.`,
|
|
21
16
|
);
|
|
22
17
|
this.name = 'UnsupportedFieldRemapDocumentVersionError';
|
|
23
18
|
this.version = version;
|
|
@@ -50,13 +45,9 @@ export function createFieldRemapDocument(
|
|
|
50
45
|
};
|
|
51
46
|
}
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
return version === FIELD_REMAP_DOCUMENT_V1_VERSION || version === FIELD_REMAP_DOCUMENT_VERSION;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Normalize edges / operators on a persisted document; always emits the current version. */
|
|
48
|
+
/** Normalize edges / operators on a current persisted document. */
|
|
58
49
|
export function normalizeFieldRemapDocument(document: FieldRemapDocument): FieldRemapDocument {
|
|
59
|
-
if (
|
|
50
|
+
if (document.version !== FIELD_REMAP_DOCUMENT_VERSION) {
|
|
60
51
|
throw new UnsupportedFieldRemapDocumentVersionError(document.version);
|
|
61
52
|
}
|
|
62
53
|
const operators = normalizeMappingOperators(document.operators);
|
|
@@ -83,8 +74,7 @@ export function serializeFieldRemapDocument(
|
|
|
83
74
|
}
|
|
84
75
|
|
|
85
76
|
/**
|
|
86
|
-
* Parse an unknown JSON value into a normalized `FieldRemapDocument`.
|
|
87
|
-
* Accepts v1 (edges-only) and v2 (optional operators); always returns current version.
|
|
77
|
+
* Parse an unknown JSON value into a normalized current `FieldRemapDocument`.
|
|
88
78
|
*/
|
|
89
79
|
export function parseFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
90
80
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
@@ -97,7 +87,7 @@ export function parseFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
|
97
87
|
if (!('version' in record)) {
|
|
98
88
|
throw new InvalidFieldRemapDocumentError('Field remap document is missing version.');
|
|
99
89
|
}
|
|
100
|
-
if (
|
|
90
|
+
if (record.version !== FIELD_REMAP_DOCUMENT_VERSION) {
|
|
101
91
|
throw new UnsupportedFieldRemapDocumentVersionError(record.version);
|
|
102
92
|
}
|
|
103
93
|
if (!Array.isArray(record.edges)) {
|
|
@@ -114,7 +104,7 @@ export function parseFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
|
114
104
|
}
|
|
115
105
|
|
|
116
106
|
return normalizeFieldRemapDocument({
|
|
117
|
-
version:
|
|
107
|
+
version: FIELD_REMAP_DOCUMENT_VERSION,
|
|
118
108
|
edges: record.edges as MappingEdge[],
|
|
119
109
|
operators: record.operators as MappingOperator[] | undefined,
|
|
120
110
|
});
|
|
@@ -131,33 +121,3 @@ export function deserializeFieldRemapDocument(json: string): FieldRemapDocument
|
|
|
131
121
|
}
|
|
132
122
|
return parseFieldRemapDocument(parsed);
|
|
133
123
|
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Migrate an unknown persisted value to the current {@link FieldRemapDocument}.
|
|
137
|
-
*
|
|
138
|
-
* Hosts should call this (or `parseFieldRemapDocument`) at load time so future
|
|
139
|
-
* document versions can be rewritten here without changing call sites.
|
|
140
|
-
*
|
|
141
|
-
* **v1:** edges-only → current version (operators omitted).
|
|
142
|
-
* **v2:** normalize edges + operators.
|
|
143
|
-
*/
|
|
144
|
-
export function migrateFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
145
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
146
|
-
throw new InvalidFieldRemapDocumentError(
|
|
147
|
-
'Expected a field remap document object with version and edges.',
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const record = input as Record<string, unknown>;
|
|
152
|
-
if (!('version' in record)) {
|
|
153
|
-
throw new InvalidFieldRemapDocumentError('Field remap document is missing version.');
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
switch (record.version) {
|
|
157
|
-
case FIELD_REMAP_DOCUMENT_V1_VERSION:
|
|
158
|
-
case FIELD_REMAP_DOCUMENT_VERSION:
|
|
159
|
-
return parseFieldRemapDocument(input);
|
|
160
|
-
default:
|
|
161
|
-
throw new UnsupportedFieldRemapDocumentVersionError(record.version);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
IDENTITY_TRANSFORM_ID,
|
|
4
|
-
MAX_TRANSFORM_CHAIN,
|
|
5
|
-
} from '../constants.js';
|
|
6
|
-
import {
|
|
7
|
-
sanitizeOptionRecord,
|
|
8
|
-
sanitizeOptionSteps,
|
|
9
|
-
sharedOptionsFromSteps,
|
|
10
|
-
} from '../mapping/transformOptions.js';
|
|
1
|
+
import { IDENTITY_TRANSFORM_ID, MAX_TRANSFORM_CHAIN } from '../constants.js';
|
|
2
|
+
import { sanitizeOptionSteps } from '../mapping/transformOptions.js';
|
|
11
3
|
import type { MappingEdge } from '../types.js';
|
|
12
4
|
|
|
13
5
|
export { MAX_TRANSFORM_CHAIN } from '../constants.js';
|
|
14
6
|
|
|
15
7
|
function sanitizeTransformIds(ids: readonly string[] | undefined): string[] {
|
|
16
8
|
const cleaned = ids
|
|
17
|
-
?.map((id) =>
|
|
9
|
+
?.map((id) => id.trim())
|
|
18
10
|
.filter((id) => id.length > 0 && id !== IDENTITY_TRANSFORM_ID);
|
|
19
11
|
if (!cleaned || cleaned.length === 0) {
|
|
20
12
|
return [];
|
|
@@ -22,23 +14,9 @@ function sanitizeTransformIds(ids: readonly string[] | undefined): string[] {
|
|
|
22
14
|
return cleaned.slice(0, MAX_TRANSFORM_CHAIN);
|
|
23
15
|
}
|
|
24
16
|
|
|
25
|
-
/**
|
|
26
|
-
* Resolve the effective transform chain for an edge.
|
|
27
|
-
* Prefers `transformIds`; falls back to legacy `transformId`.
|
|
28
|
-
* Identity / empty / null → `[]` (pass-through).
|
|
29
|
-
*/
|
|
17
|
+
/** Resolve the effective transform chain for an edge. Identity / empty → pass-through. */
|
|
30
18
|
export function edgeTransformIds(edge: MappingEdge): string[] {
|
|
31
|
-
|
|
32
|
-
if (fromList.length > 0) {
|
|
33
|
-
return fromList;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const legacy = edge.transformId ? canonicalizeTransformId(edge.transformId) : '';
|
|
37
|
-
if (legacy && legacy !== IDENTITY_TRANSFORM_ID) {
|
|
38
|
-
return [legacy];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return [];
|
|
19
|
+
return sanitizeTransformIds(edge.transformIds);
|
|
42
20
|
}
|
|
43
21
|
|
|
44
22
|
/** Per-item transform chain applied after optional `itemSourcePath` projection. */
|
|
@@ -46,15 +24,7 @@ export function edgeItemTransformIds(edge: MappingEdge): string[] {
|
|
|
46
24
|
return sanitizeTransformIds(edge.itemTransformIds);
|
|
47
25
|
}
|
|
48
26
|
|
|
49
|
-
/**
|
|
50
|
-
* Normalize an edge to the `transformIds` model while keeping `transformId`
|
|
51
|
-
* as the first chain step (or `null`) for older hosts.
|
|
52
|
-
*
|
|
53
|
-
* Option bags:
|
|
54
|
-
* - Prefer `transformOptionSteps` when present (aligned to chain length).
|
|
55
|
-
* - Keep `transformOptions` as the first non-empty step (or the legacy shared bag).
|
|
56
|
-
* - Same rules for `itemTransformOptionSteps` / `itemTransformOptions`.
|
|
57
|
-
*/
|
|
27
|
+
/** Normalize transform chains and their aligned per-step option bags. */
|
|
58
28
|
export function normalizeMappingEdge(edge: MappingEdge): MappingEdge {
|
|
59
29
|
const ids = edgeTransformIds(edge);
|
|
60
30
|
const itemIds = edgeItemTransformIds(edge);
|
|
@@ -69,29 +39,20 @@ export function normalizeMappingEdge(edge: MappingEdge): MappingEdge {
|
|
|
69
39
|
: undefined;
|
|
70
40
|
|
|
71
41
|
const transformOptionSteps = sanitizeOptionSteps(edge.transformOptionSteps, ids.length);
|
|
72
|
-
const transformOptions =
|
|
73
|
-
sharedOptionsFromSteps(transformOptionSteps) ?? sanitizeOptionRecord(edge.transformOptions);
|
|
74
|
-
|
|
75
42
|
const itemTransformOptionSteps = sanitizeOptionSteps(
|
|
76
43
|
edge.itemTransformOptionSteps,
|
|
77
44
|
itemIds.length,
|
|
78
45
|
);
|
|
79
|
-
const itemTransformOptions =
|
|
80
|
-
sharedOptionsFromSteps(itemTransformOptionSteps) ??
|
|
81
|
-
sanitizeOptionRecord(edge.itemTransformOptions);
|
|
82
46
|
|
|
83
47
|
return {
|
|
84
48
|
id: edge.id,
|
|
85
49
|
sourceFieldId: edge.sourceFieldId,
|
|
86
50
|
targetSlotId: edge.targetSlotId,
|
|
87
51
|
transformIds: ids.length > 0 ? ids : undefined,
|
|
88
|
-
transformId: ids[0] ?? null,
|
|
89
52
|
...(transformOptionSteps ? { transformOptionSteps } : {}),
|
|
90
|
-
...(transformOptions ? { transformOptions } : {}),
|
|
91
53
|
...(itemSourcePath && !itemEdges ? { itemSourcePath } : {}),
|
|
92
54
|
...(itemIds.length > 0 && !itemEdges ? { itemTransformIds: itemIds } : {}),
|
|
93
55
|
...(itemTransformOptionSteps && !itemEdges ? { itemTransformOptionSteps } : {}),
|
|
94
|
-
...(itemTransformOptions && !itemEdges ? { itemTransformOptions } : {}),
|
|
95
56
|
...(itemEdges ? { itemEdges } : {}),
|
|
96
57
|
};
|
|
97
58
|
}
|
|
@@ -106,16 +67,12 @@ export function createMappingEdge(input: {
|
|
|
106
67
|
readonly sourceFieldId: string;
|
|
107
68
|
readonly targetSlotId: string;
|
|
108
69
|
readonly transformIds?: readonly string[];
|
|
109
|
-
/** Legacy single id; ignored when `transformIds` is provided. */
|
|
110
|
-
readonly transformId?: string | null;
|
|
111
70
|
readonly transformOptionSteps?: readonly (Readonly<Record<string, unknown>> | undefined)[];
|
|
112
|
-
readonly transformOptions?: Readonly<Record<string, unknown>>;
|
|
113
71
|
/** Optional per-item projection path for array mappings. */
|
|
114
72
|
readonly itemSourcePath?: string;
|
|
115
73
|
/** Optional per-item transform chain after projection. */
|
|
116
74
|
readonly itemTransformIds?: readonly string[];
|
|
117
75
|
readonly itemTransformOptionSteps?: readonly (Readonly<Record<string, unknown>> | undefined)[];
|
|
118
|
-
readonly itemTransformOptions?: Readonly<Record<string, unknown>>;
|
|
119
76
|
/** List-context child edges for array-of-object → array-of-object. */
|
|
120
77
|
readonly itemEdges?: readonly MappingEdge[];
|
|
121
78
|
}): MappingEdge {
|
|
@@ -124,13 +81,10 @@ export function createMappingEdge(input: {
|
|
|
124
81
|
sourceFieldId: input.sourceFieldId,
|
|
125
82
|
targetSlotId: input.targetSlotId,
|
|
126
83
|
transformIds: input.transformIds,
|
|
127
|
-
transformId: input.transformId,
|
|
128
84
|
transformOptionSteps: input.transformOptionSteps,
|
|
129
|
-
transformOptions: input.transformOptions,
|
|
130
85
|
itemSourcePath: input.itemSourcePath,
|
|
131
86
|
itemTransformIds: input.itemTransformIds,
|
|
132
87
|
itemTransformOptionSteps: input.itemTransformOptionSteps,
|
|
133
|
-
itemTransformOptions: input.itemTransformOptions,
|
|
134
88
|
itemEdges: input.itemEdges,
|
|
135
89
|
});
|
|
136
90
|
}
|
|
@@ -75,7 +75,7 @@ export async function convertArrayWithItemEdges(input: {
|
|
|
75
75
|
? await identity.apply(sourceValue, { ...input.context, sampleValue: sourceValue })
|
|
76
76
|
: sourceValue;
|
|
77
77
|
} else {
|
|
78
|
-
const steps = resolveOptionSteps(chain, edge.transformOptionSteps
|
|
78
|
+
const steps = resolveOptionSteps(chain, edge.transformOptionSteps);
|
|
79
79
|
value = await applyTransformChain(
|
|
80
80
|
input.transforms,
|
|
81
81
|
chain,
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
* {@link applyMappingOperators} explicitly without persisting.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
canonicalizeTransformId,
|
|
9
|
-
IDENTITY_TRANSFORM_ID,
|
|
10
|
-
MAX_TRANSFORM_CHAIN,
|
|
11
|
-
} from '../constants.js';
|
|
7
|
+
import { IDENTITY_TRANSFORM_ID, MAX_TRANSFORM_CHAIN } from '../constants.js';
|
|
12
8
|
import { throwIfAborted } from '../abort.js';
|
|
13
9
|
import type {
|
|
14
10
|
CombineMappingOperator,
|
|
@@ -113,7 +109,7 @@ function readFieldValue(field: SourceField, inputs: Readonly<Record<string, unkn
|
|
|
113
109
|
|
|
114
110
|
function sanitizeOperatorTransformIds(ids: readonly string[] | undefined): string[] | undefined {
|
|
115
111
|
const cleaned = ids
|
|
116
|
-
?.map((id) =>
|
|
112
|
+
?.map((id) => id.trim())
|
|
117
113
|
.filter((id) => id.length > 0 && id !== IDENTITY_TRANSFORM_ID);
|
|
118
114
|
if (!cleaned || cleaned.length === 0) {
|
|
119
115
|
return undefined;
|
|
@@ -103,21 +103,6 @@ export function parseObjectPath(path: string): ObjectPathSegment[] {
|
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
* Parse dotted path segments as plain property names (no `[index]` / `[*]`).
|
|
108
|
-
* Rejects unsafe segments when any parts exist.
|
|
109
|
-
*/
|
|
110
|
-
export function requireObjectPathParts(path: string): string[] {
|
|
111
|
-
const segments = parseObjectPath(path);
|
|
112
|
-
if (segments.some((segment) => segment.kind !== 'property')) {
|
|
113
|
-
throw new InvalidObjectPathError(
|
|
114
|
-
path,
|
|
115
|
-
'index/wildcard segments are not allowed in this context',
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
return segments.map((segment) => segment.name);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
106
|
export function isSafeObjectPath(path: string): boolean {
|
|
122
107
|
const trimmed = path.trim();
|
|
123
108
|
if (!SAFE_PATH_RE.test(trimmed)) {
|
|
@@ -28,10 +28,9 @@ export function findSourceField(
|
|
|
28
28
|
* Apply order:
|
|
29
29
|
* 1. Optional `itemSourcePath` projection (array of objects → projected array)
|
|
30
30
|
* 2. Optional `itemTransformIds` per element (when the value is still an array)
|
|
31
|
-
* 3. `transformIds`
|
|
31
|
+
* 3. `transformIds` on the whole value (including array reduces)
|
|
32
32
|
*
|
|
33
|
-
* Per-step options
|
|
34
|
-
* present; otherwise shared `transformOptions` / `itemTransformOptions` apply to all steps.
|
|
33
|
+
* Per-step options are aligned through `transformOptionSteps` / `itemTransformOptionSteps`.
|
|
35
34
|
*/
|
|
36
35
|
export async function resolveMappedValue(
|
|
37
36
|
edge: MappingEdge,
|
|
@@ -44,11 +43,7 @@ export async function resolveMappedValue(
|
|
|
44
43
|
: sourceValue;
|
|
45
44
|
|
|
46
45
|
const itemChain = edgeItemTransformIds(edge);
|
|
47
|
-
const itemSteps = resolveOptionSteps(
|
|
48
|
-
itemChain,
|
|
49
|
-
edge.itemTransformOptionSteps,
|
|
50
|
-
edge.itemTransformOptions,
|
|
51
|
-
);
|
|
46
|
+
const itemSteps = resolveOptionSteps(itemChain, edge.itemTransformOptionSteps);
|
|
52
47
|
|
|
53
48
|
if (itemChain.length > 0 && Array.isArray(current)) {
|
|
54
49
|
current = await Promise.all(
|
|
@@ -62,7 +57,7 @@ export async function resolveMappedValue(
|
|
|
62
57
|
return identity ? await identity.apply(current, context) : current;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
const valueSteps = resolveOptionSteps(chain, edge.transformOptionSteps
|
|
60
|
+
const valueSteps = resolveOptionSteps(chain, edge.transformOptionSteps);
|
|
66
61
|
return applyTransformChain(registry, chain, current, context, valueSteps);
|
|
67
62
|
}
|
|
68
63
|
|
|
@@ -1,39 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
/** Merge edge-local options over host `context.options` (edge wins). */
|
|
4
|
-
export function contextWithEdgeOptions(
|
|
5
|
-
context: TransformContext,
|
|
6
|
-
edgeOptions: Readonly<Record<string, unknown>> | undefined,
|
|
7
|
-
): TransformContext {
|
|
8
|
-
if (!edgeOptions || Object.keys(edgeOptions).length === 0) {
|
|
9
|
-
return context;
|
|
10
|
-
}
|
|
11
|
-
return {
|
|
12
|
-
...context,
|
|
13
|
-
options: {
|
|
14
|
-
...context.options,
|
|
15
|
-
...edgeOptions,
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Collect unique `optionFields` declared by transforms in a chain (later ids win per key). */
|
|
21
|
-
export function collectOptionFields(
|
|
22
|
-
registry: ValueTransformRegistry,
|
|
23
|
-
transformIds: readonly string[],
|
|
24
|
-
): TransformOptionField[] {
|
|
25
|
-
const byKey = new Map<string, TransformOptionField>();
|
|
26
|
-
for (const id of transformIds) {
|
|
27
|
-
const fields = registry.get(id)?.optionFields;
|
|
28
|
-
if (!fields) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
for (const field of fields) {
|
|
32
|
-
byKey.set(field.key, field);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return [...byKey.values()];
|
|
36
|
-
}
|
|
1
|
+
import type { TransformOptionField, ValueTransformRegistry } from '../types.js';
|
|
37
2
|
|
|
38
3
|
/** Option fields for a single chain step. */
|
|
39
4
|
export function optionFieldsForStep(
|
|
@@ -63,17 +28,6 @@ export function sanitizeOptionRecord(
|
|
|
63
28
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
64
29
|
}
|
|
65
30
|
|
|
66
|
-
export function patchOptionRecord(
|
|
67
|
-
previous: Readonly<Record<string, unknown>> | undefined,
|
|
68
|
-
key: string,
|
|
69
|
-
value: unknown,
|
|
70
|
-
): Readonly<Record<string, unknown>> | undefined {
|
|
71
|
-
return sanitizeOptionRecord({
|
|
72
|
-
...previous,
|
|
73
|
-
[key]: value,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
31
|
/**
|
|
78
32
|
* Align / sanitize per-step option bags to `length`.
|
|
79
33
|
* Returns `undefined` when every step is empty.
|
|
@@ -97,79 +51,16 @@ export function sanitizeOptionSteps(
|
|
|
97
51
|
return any ? next : undefined;
|
|
98
52
|
}
|
|
99
53
|
|
|
100
|
-
/**
|
|
101
|
-
* Resolve per-step options for a transform chain.
|
|
102
|
-
* Prefers `steps`; otherwise expands shared `transformOptions` to every step (apply-to-all).
|
|
103
|
-
*/
|
|
54
|
+
/** Resolve sanitized per-step options aligned to a transform chain. */
|
|
104
55
|
export function resolveOptionSteps(
|
|
105
56
|
transformIds: readonly string[],
|
|
106
57
|
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
107
|
-
shared: Readonly<Record<string, unknown>> | undefined,
|
|
108
58
|
): (Readonly<Record<string, unknown>> | undefined)[] {
|
|
109
59
|
const length = transformIds.length;
|
|
110
60
|
if (length === 0) {
|
|
111
61
|
return [];
|
|
112
62
|
}
|
|
113
|
-
|
|
114
|
-
return Array.from({ length }, (_, index) => sanitizeOptionRecord(steps[index]));
|
|
115
|
-
}
|
|
116
|
-
const bag = sanitizeOptionRecord(shared);
|
|
117
|
-
if (!bag) {
|
|
118
|
-
return Array.from({ length }, () => undefined);
|
|
119
|
-
}
|
|
120
|
-
return Array.from({ length }, () => bag);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Back-compat summary: first non-empty step bag (else undefined). */
|
|
124
|
-
export function sharedOptionsFromSteps(
|
|
125
|
-
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
126
|
-
): Readonly<Record<string, unknown>> | undefined {
|
|
127
|
-
if (!steps) {
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
for (const step of steps) {
|
|
131
|
-
const sanitized = sanitizeOptionRecord(step);
|
|
132
|
-
if (sanitized) {
|
|
133
|
-
return sanitized;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Merge all step bags (later steps win) — useful for live format-sample chips. */
|
|
140
|
-
export function mergeOptionSteps(
|
|
141
|
-
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
142
|
-
): Readonly<Record<string, unknown>> | undefined {
|
|
143
|
-
if (!steps || steps.length === 0) {
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
const merged: Record<string, unknown> = {};
|
|
147
|
-
for (const step of steps) {
|
|
148
|
-
if (!step) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
Object.assign(merged, step);
|
|
152
|
-
}
|
|
153
|
-
return sanitizeOptionRecord(merged);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function patchOptionStep(
|
|
157
|
-
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
158
|
-
length: number,
|
|
159
|
-
index: number,
|
|
160
|
-
key: string,
|
|
161
|
-
value: unknown,
|
|
162
|
-
sharedFallback?: Readonly<Record<string, unknown>>,
|
|
163
|
-
): readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined {
|
|
164
|
-
const base = resolveOptionSteps(
|
|
165
|
-
Array.from({ length }, () => ''),
|
|
166
|
-
steps,
|
|
167
|
-
sharedFallback,
|
|
168
|
-
);
|
|
169
|
-
const next = base.map((step, stepIndex) =>
|
|
170
|
-
stepIndex === index ? patchOptionRecord(step, key, value) : step,
|
|
171
|
-
);
|
|
172
|
-
return sanitizeOptionSteps(next, length);
|
|
63
|
+
return Array.from({ length }, (_, index) => sanitizeOptionRecord(steps?.[index]));
|
|
173
64
|
}
|
|
174
65
|
|
|
175
66
|
export function resizeOptionSteps(
|
package/src/domain/types.ts
CHANGED
|
@@ -81,28 +81,15 @@ export interface MappingEdge {
|
|
|
81
81
|
readonly targetSlotId: string;
|
|
82
82
|
/**
|
|
83
83
|
* Ordered transform chain (max 3). Empty / omitted means identity.
|
|
84
|
-
* Prefer this over `transformId` for new writers.
|
|
85
84
|
* For arrays: use reduce builtins (`array:join`, `array:first`, …) here after
|
|
86
85
|
* optional item projection / item transforms.
|
|
87
86
|
*/
|
|
88
87
|
readonly transformIds?: readonly string[];
|
|
89
|
-
/**
|
|
90
|
-
* Legacy single transform. Prefer `transformIds`.
|
|
91
|
-
* Readers should use `edgeTransformIds` / `normalizeMappingEdge`.
|
|
92
|
-
* `null` / omitted (with no `transformIds`) means identity (pass-through).
|
|
93
|
-
*/
|
|
94
|
-
readonly transformId?: string | null;
|
|
95
88
|
/**
|
|
96
89
|
* Per-step options aligned with `transformIds` (index N applies to step N).
|
|
97
|
-
*
|
|
90
|
+
* Steps may use different bags (e.g. `showSeconds` then `maxLength`).
|
|
98
91
|
*/
|
|
99
92
|
readonly transformOptionSteps?: readonly (Readonly<Record<string, unknown>> | undefined)[];
|
|
100
|
-
/**
|
|
101
|
-
* Shared options for all `transformIds` steps (legacy / apply-to-all).
|
|
102
|
-
* Used when `transformOptionSteps` is omitted. Still written as a back-compat
|
|
103
|
-
* summary of step 0 (or the first non-empty step) by `normalizeMappingEdge`.
|
|
104
|
-
*/
|
|
105
|
-
readonly transformOptions?: Readonly<Record<string, unknown>>;
|
|
106
93
|
/**
|
|
107
94
|
* When the source is an array of objects, optional dotted path into each item
|
|
108
95
|
* (e.g. `name` or `meta.label`) before the value is written to the target.
|
|
@@ -120,11 +107,6 @@ export interface MappingEdge {
|
|
|
120
107
|
* Per-step options aligned with `itemTransformIds`.
|
|
121
108
|
*/
|
|
122
109
|
readonly itemTransformOptionSteps?: readonly (Readonly<Record<string, unknown>> | undefined)[];
|
|
123
|
-
/**
|
|
124
|
-
* Shared options for all `itemTransformIds` steps (legacy / apply-to-all).
|
|
125
|
-
* Independent of `transformOptions` / `transformOptionSteps`.
|
|
126
|
-
*/
|
|
127
|
-
readonly itemTransformOptions?: Readonly<Record<string, unknown>>;
|
|
128
110
|
/**
|
|
129
111
|
* List-context child bindings (Stedi-style): when source is an array of objects,
|
|
130
112
|
* each element is converted through these edges into a target item object.
|
|
@@ -161,16 +143,14 @@ export type MappingOperator = CombineMappingOperator | SplitMappingOperator;
|
|
|
161
143
|
|
|
162
144
|
/**
|
|
163
145
|
* Minimal JSON-serializable mapping document for host persistence.
|
|
164
|
-
* Hosts own schema trees; this document stores the binding graph
|
|
165
|
-
* n→m operators from document v2 onward).
|
|
146
|
+
* Hosts own schema trees; this document stores the binding graph and optional n→m operators.
|
|
166
147
|
*/
|
|
167
148
|
export interface FieldRemapDocument {
|
|
168
|
-
|
|
169
|
-
readonly version: 1 | 2;
|
|
149
|
+
readonly version: 2;
|
|
170
150
|
readonly edges: readonly MappingEdge[];
|
|
171
151
|
/**
|
|
172
|
-
* Optional n→m combine/split operators
|
|
173
|
-
* Omitted / empty on
|
|
152
|
+
* Optional n→m combine/split operators.
|
|
153
|
+
* Omitted / empty on hosts that only use 1→1 edges.
|
|
174
154
|
*/
|
|
175
155
|
readonly operators?: readonly MappingOperator[];
|
|
176
156
|
}
|
|
@@ -223,8 +203,7 @@ export interface ValueTransformDefinition {
|
|
|
223
203
|
/**
|
|
224
204
|
* Data-driven option editors (mapped rows / convert panel).
|
|
225
205
|
* Values are stored per step (`transformOptionSteps` / `itemTransformOptionSteps`)
|
|
226
|
-
* or
|
|
227
|
-
* `TransformContext.options`.
|
|
206
|
+
* or on host `TransformContext.options`.
|
|
228
207
|
*/
|
|
229
208
|
readonly optionFields?: readonly TransformOptionField[];
|
|
230
209
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,12 +15,7 @@ export type {
|
|
|
15
15
|
ValueTransformRegistry,
|
|
16
16
|
} from './domain/types.js';
|
|
17
17
|
|
|
18
|
-
export {
|
|
19
|
-
canonicalizeTransformId,
|
|
20
|
-
IDENTITY_TRANSFORM_ID,
|
|
21
|
-
MAX_TRANSFORM_CHAIN,
|
|
22
|
-
TRANSFORM_ID_ALIASES,
|
|
23
|
-
} from './domain/constants.js';
|
|
18
|
+
export { IDENTITY_TRANSFORM_ID, MAX_TRANSFORM_CHAIN } from './domain/constants.js';
|
|
24
19
|
|
|
25
20
|
export {
|
|
26
21
|
createMappingEdge,
|
|
@@ -34,11 +29,9 @@ export {
|
|
|
34
29
|
createFieldRemapDocument,
|
|
35
30
|
deserializeFieldRemapDocument,
|
|
36
31
|
InvalidFieldRemapDocumentError,
|
|
37
|
-
migrateFieldRemapDocument,
|
|
38
32
|
normalizeFieldRemapDocument,
|
|
39
33
|
parseFieldRemapDocument,
|
|
40
34
|
FIELD_REMAP_DOCUMENT_VERSION,
|
|
41
|
-
FIELD_REMAP_DOCUMENT_V1_VERSION,
|
|
42
35
|
serializeFieldRemapDocument,
|
|
43
36
|
UnsupportedFieldRemapDocumentVersionError,
|
|
44
37
|
} from './domain/document/fieldRemapDocument.js';
|
|
@@ -93,17 +86,11 @@ export type {
|
|
|
93
86
|
} from './domain/mapping/mappingOperators.js';
|
|
94
87
|
|
|
95
88
|
export {
|
|
96
|
-
collectOptionFields,
|
|
97
|
-
contextWithEdgeOptions,
|
|
98
|
-
mergeOptionSteps,
|
|
99
89
|
optionFieldsForStep,
|
|
100
|
-
patchOptionRecord,
|
|
101
|
-
patchOptionStep,
|
|
102
90
|
resolveOptionSteps,
|
|
103
91
|
resizeOptionSteps,
|
|
104
92
|
sanitizeOptionRecord,
|
|
105
93
|
sanitizeOptionSteps,
|
|
106
|
-
sharedOptionsFromSteps,
|
|
107
94
|
} from './domain/mapping/transformOptions.js';
|
|
108
95
|
|
|
109
96
|
export {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { throwIfAborted } from '../domain/abort.js';
|
|
2
|
-
import {
|
|
2
|
+
import { MAX_TRANSFORM_CHAIN } from '../domain/constants.js';
|
|
3
3
|
import type {
|
|
4
4
|
FieldDataType,
|
|
5
5
|
TransformContext,
|
|
@@ -18,8 +18,7 @@ export function createValueTransformRegistry(
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function resolve(id: string): ValueTransformDefinition | undefined {
|
|
21
|
-
|
|
22
|
-
return byId.get(canonical) ?? byId.get(id);
|
|
21
|
+
return byId.get(id.trim()) ?? byId.get(id);
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
return {
|