@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
|
@@ -1,96 +1,98 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
MappingEdge,
|
|
3
|
-
SourceField,
|
|
4
|
-
TransformContext,
|
|
5
|
-
ValueTransformRegistry,
|
|
6
|
-
} from '../types.js';
|
|
7
|
-
import { edgeItemTransformIds, edgeTransformIds } from '../document/mappingEdge.js';
|
|
8
|
-
import { projectCollectionItems } from './pathUtils.js';
|
|
9
|
-
import { applyTransformChain } from '../../registry/createValueTransformRegistry.js';
|
|
10
|
-
import { BUILTIN_TRANSFORM_IDS } from '../../registry/builtinTransforms.js';
|
|
11
|
-
import { resolveOptionSteps } from './transformOptions.js';
|
|
12
|
-
import { flattenSourceFields } from './treeUtils.js';
|
|
13
|
-
|
|
14
|
-
export { findTargetSlot, flattenSourceFields, flattenTargetSlots } from './treeUtils.js';
|
|
15
|
-
|
|
16
|
-
export { projectCollectionItems, readObjectPath } from './pathUtils.js';
|
|
17
|
-
|
|
18
|
-
export function findSourceField(
|
|
19
|
-
fields: readonly SourceField[],
|
|
20
|
-
fieldId: string,
|
|
21
|
-
): SourceField | undefined {
|
|
22
|
-
return flattenSourceFields(fields).find((field) => field.id === fieldId);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Resolve an edge against a live / sample source value.
|
|
27
|
-
*
|
|
28
|
-
* Apply order:
|
|
29
|
-
* 1. Optional `itemSourcePath` projection (array of objects → projected array)
|
|
30
|
-
* 2. Optional `itemTransformIds` per element (when the value is still an array)
|
|
31
|
-
* 3. `transformIds` / legacy `transformId` on the whole value (including array reduces)
|
|
32
|
-
*
|
|
33
|
-
* Per-step options (`transformOptionSteps` / `itemTransformOptionSteps`) win when
|
|
34
|
-
* present; otherwise shared `transformOptions` / `itemTransformOptions` apply to all steps.
|
|
35
|
-
*/
|
|
36
|
-
export function resolveMappedValue(
|
|
37
|
-
edge: MappingEdge,
|
|
38
|
-
sourceValue: unknown,
|
|
39
|
-
registry: ValueTransformRegistry,
|
|
40
|
-
context: TransformContext = {},
|
|
41
|
-
): unknown {
|
|
42
|
-
let current = edge.itemSourcePath
|
|
43
|
-
? projectCollectionItems(sourceValue, edge.itemSourcePath)
|
|
44
|
-
: sourceValue;
|
|
45
|
-
|
|
46
|
-
const itemChain = edgeItemTransformIds(edge);
|
|
47
|
-
const itemSteps = resolveOptionSteps(
|
|
48
|
-
itemChain,
|
|
49
|
-
edge.itemTransformOptionSteps,
|
|
50
|
-
edge.itemTransformOptions,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
if (itemChain.length > 0 && Array.isArray(current)) {
|
|
54
|
-
current =
|
|
55
|
-
applyTransformChain(registry, itemChain, item, context, itemSteps),
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const chain = edgeTransformIds(edge);
|
|
60
|
-
if (chain.length === 0) {
|
|
61
|
-
const identity = registry.get(BUILTIN_TRANSFORM_IDS.identity);
|
|
62
|
-
return identity ? identity.apply(current, context) : current;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const valueSteps = resolveOptionSteps(chain, edge.transformOptionSteps, edge.transformOptions);
|
|
66
|
-
return applyTransformChain(registry, chain, current, context, valueSteps);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function resolveEdgePreview(
|
|
70
|
-
edge: MappingEdge,
|
|
71
|
-
sources: readonly SourceField[],
|
|
72
|
-
registry: ValueTransformRegistry,
|
|
73
|
-
context: TransformContext = {},
|
|
74
|
-
): unknown {
|
|
75
|
-
const field = findSourceField(sources, edge.sourceFieldId);
|
|
76
|
-
const sample =
|
|
77
|
-
context.sampleValue !== undefined ? context.sampleValue : (field?.sampleValue ?? context.now);
|
|
78
|
-
return resolveMappedValue(edge, sample, registry, {
|
|
79
|
-
...context,
|
|
80
|
-
sampleValue: sample,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Resolve every edge into `{ targetSlotId, value }` for live preview panels. */
|
|
85
|
-
export function resolveAllEdgePreviews(
|
|
86
|
-
edges: readonly MappingEdge[],
|
|
87
|
-
sources: readonly SourceField[],
|
|
88
|
-
registry: ValueTransformRegistry,
|
|
89
|
-
context: TransformContext = {},
|
|
90
|
-
): ReadonlyArray<{ edgeId: string; targetSlotId: string; value: unknown }
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
MappingEdge,
|
|
3
|
+
SourceField,
|
|
4
|
+
TransformContext,
|
|
5
|
+
ValueTransformRegistry,
|
|
6
|
+
} from '../types.js';
|
|
7
|
+
import { edgeItemTransformIds, edgeTransformIds } from '../document/mappingEdge.js';
|
|
8
|
+
import { projectCollectionItems } from './pathUtils.js';
|
|
9
|
+
import { applyTransformChain } from '../../registry/createValueTransformRegistry.js';
|
|
10
|
+
import { BUILTIN_TRANSFORM_IDS } from '../../registry/builtinTransforms.js';
|
|
11
|
+
import { resolveOptionSteps } from './transformOptions.js';
|
|
12
|
+
import { flattenSourceFields } from './treeUtils.js';
|
|
13
|
+
|
|
14
|
+
export { findTargetSlot, flattenSourceFields, flattenTargetSlots } from './treeUtils.js';
|
|
15
|
+
|
|
16
|
+
export { projectCollectionItems, readObjectPath } from './pathUtils.js';
|
|
17
|
+
|
|
18
|
+
export function findSourceField(
|
|
19
|
+
fields: readonly SourceField[],
|
|
20
|
+
fieldId: string,
|
|
21
|
+
): SourceField | undefined {
|
|
22
|
+
return flattenSourceFields(fields).find((field) => field.id === fieldId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve an edge against a live / sample source value.
|
|
27
|
+
*
|
|
28
|
+
* Apply order:
|
|
29
|
+
* 1. Optional `itemSourcePath` projection (array of objects → projected array)
|
|
30
|
+
* 2. Optional `itemTransformIds` per element (when the value is still an array)
|
|
31
|
+
* 3. `transformIds` / legacy `transformId` on the whole value (including array reduces)
|
|
32
|
+
*
|
|
33
|
+
* Per-step options (`transformOptionSteps` / `itemTransformOptionSteps`) win when
|
|
34
|
+
* present; otherwise shared `transformOptions` / `itemTransformOptions` apply to all steps.
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveMappedValue(
|
|
37
|
+
edge: MappingEdge,
|
|
38
|
+
sourceValue: unknown,
|
|
39
|
+
registry: ValueTransformRegistry,
|
|
40
|
+
context: TransformContext = {},
|
|
41
|
+
): Promise<unknown> {
|
|
42
|
+
let current = edge.itemSourcePath
|
|
43
|
+
? projectCollectionItems(sourceValue, edge.itemSourcePath)
|
|
44
|
+
: sourceValue;
|
|
45
|
+
|
|
46
|
+
const itemChain = edgeItemTransformIds(edge);
|
|
47
|
+
const itemSteps = resolveOptionSteps(
|
|
48
|
+
itemChain,
|
|
49
|
+
edge.itemTransformOptionSteps,
|
|
50
|
+
edge.itemTransformOptions,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (itemChain.length > 0 && Array.isArray(current)) {
|
|
54
|
+
current = await Promise.all(
|
|
55
|
+
current.map((item) => applyTransformChain(registry, itemChain, item, context, itemSteps)),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const chain = edgeTransformIds(edge);
|
|
60
|
+
if (chain.length === 0) {
|
|
61
|
+
const identity = registry.get(BUILTIN_TRANSFORM_IDS.identity);
|
|
62
|
+
return identity ? await identity.apply(current, context) : current;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const valueSteps = resolveOptionSteps(chain, edge.transformOptionSteps, edge.transformOptions);
|
|
66
|
+
return applyTransformChain(registry, chain, current, context, valueSteps);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function resolveEdgePreview(
|
|
70
|
+
edge: MappingEdge,
|
|
71
|
+
sources: readonly SourceField[],
|
|
72
|
+
registry: ValueTransformRegistry,
|
|
73
|
+
context: TransformContext = {},
|
|
74
|
+
): Promise<unknown> {
|
|
75
|
+
const field = findSourceField(sources, edge.sourceFieldId);
|
|
76
|
+
const sample =
|
|
77
|
+
context.sampleValue !== undefined ? context.sampleValue : (field?.sampleValue ?? context.now);
|
|
78
|
+
return resolveMappedValue(edge, sample, registry, {
|
|
79
|
+
...context,
|
|
80
|
+
sampleValue: sample,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolve every edge into `{ targetSlotId, value }` for live preview panels. */
|
|
85
|
+
export async function resolveAllEdgePreviews(
|
|
86
|
+
edges: readonly MappingEdge[],
|
|
87
|
+
sources: readonly SourceField[],
|
|
88
|
+
registry: ValueTransformRegistry,
|
|
89
|
+
context: TransformContext = {},
|
|
90
|
+
): Promise<ReadonlyArray<{ edgeId: string; targetSlotId: string; value: unknown }>> {
|
|
91
|
+
return Promise.all(
|
|
92
|
+
edges.map(async (edge) => ({
|
|
93
|
+
edgeId: edge.id,
|
|
94
|
+
targetSlotId: edge.targetSlotId,
|
|
95
|
+
value: await resolveEdgePreview(edge, sources, registry, context),
|
|
96
|
+
})),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
import type { TransformContext, TransformOptionField, ValueTransformRegistry } from '../types.js';
|
|
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
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Option fields for a single chain step. */
|
|
39
|
-
export function optionFieldsForStep(
|
|
40
|
-
registry: ValueTransformRegistry,
|
|
41
|
-
transformId: string | undefined,
|
|
42
|
-
): TransformOptionField[] {
|
|
43
|
-
if (!transformId) {
|
|
44
|
-
return [];
|
|
45
|
-
}
|
|
46
|
-
return [...(registry.get(transformId)?.optionFields ?? [])];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Drop empty / undefined option bags when persisting edges. */
|
|
50
|
-
export function sanitizeOptionRecord(
|
|
51
|
-
options: Readonly<Record<string, unknown>> | undefined,
|
|
52
|
-
): Readonly<Record<string, unknown>> | undefined {
|
|
53
|
-
if (!options) {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
const next: Record<string, unknown> = {};
|
|
57
|
-
for (const [key, value] of Object.entries(options)) {
|
|
58
|
-
if (value === undefined) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
next[key] = value;
|
|
62
|
-
}
|
|
63
|
-
return Object.keys(next).length > 0 ? next : undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
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
|
-
/**
|
|
78
|
-
* Align / sanitize per-step option bags to `length`.
|
|
79
|
-
* Returns `undefined` when every step is empty.
|
|
80
|
-
*/
|
|
81
|
-
export function sanitizeOptionSteps(
|
|
82
|
-
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
83
|
-
length: number,
|
|
84
|
-
): readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined {
|
|
85
|
-
if (length <= 0) {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
const next: (Readonly<Record<string, unknown>> | undefined)[] = [];
|
|
89
|
-
let any = false;
|
|
90
|
-
for (let index = 0; index < length; index += 1) {
|
|
91
|
-
const sanitized = sanitizeOptionRecord(steps?.[index]);
|
|
92
|
-
next.push(sanitized);
|
|
93
|
-
if (sanitized) {
|
|
94
|
-
any = true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return any ? next : undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Resolve per-step options for a transform chain.
|
|
102
|
-
* Prefers `steps`; otherwise expands shared `transformOptions` to every step (apply-to-all).
|
|
103
|
-
*/
|
|
104
|
-
export function resolveOptionSteps(
|
|
105
|
-
transformIds: readonly string[],
|
|
106
|
-
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
107
|
-
shared: Readonly<Record<string, unknown>> | undefined,
|
|
108
|
-
): (Readonly<Record<string, unknown>> | undefined)[] {
|
|
109
|
-
const length = transformIds.length;
|
|
110
|
-
if (length === 0) {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
if (steps && steps.length > 0) {
|
|
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);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function resizeOptionSteps(
|
|
176
|
-
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
177
|
-
length: number,
|
|
178
|
-
): readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined {
|
|
179
|
-
if (length <= 0) {
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
|
-
const next = Array.from({ length }, (_, index) => sanitizeOptionRecord(steps?.[index]));
|
|
183
|
-
return sanitizeOptionSteps(next, length);
|
|
184
|
-
}
|
|
1
|
+
import type { TransformContext, TransformOptionField, ValueTransformRegistry } from '../types.js';
|
|
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
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Option fields for a single chain step. */
|
|
39
|
+
export function optionFieldsForStep(
|
|
40
|
+
registry: ValueTransformRegistry,
|
|
41
|
+
transformId: string | undefined,
|
|
42
|
+
): TransformOptionField[] {
|
|
43
|
+
if (!transformId) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
return [...(registry.get(transformId)?.optionFields ?? [])];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Drop empty / undefined option bags when persisting edges. */
|
|
50
|
+
export function sanitizeOptionRecord(
|
|
51
|
+
options: Readonly<Record<string, unknown>> | undefined,
|
|
52
|
+
): Readonly<Record<string, unknown>> | undefined {
|
|
53
|
+
if (!options) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const next: Record<string, unknown> = {};
|
|
57
|
+
for (const [key, value] of Object.entries(options)) {
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
next[key] = value;
|
|
62
|
+
}
|
|
63
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
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
|
+
/**
|
|
78
|
+
* Align / sanitize per-step option bags to `length`.
|
|
79
|
+
* Returns `undefined` when every step is empty.
|
|
80
|
+
*/
|
|
81
|
+
export function sanitizeOptionSteps(
|
|
82
|
+
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
83
|
+
length: number,
|
|
84
|
+
): readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined {
|
|
85
|
+
if (length <= 0) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const next: (Readonly<Record<string, unknown>> | undefined)[] = [];
|
|
89
|
+
let any = false;
|
|
90
|
+
for (let index = 0; index < length; index += 1) {
|
|
91
|
+
const sanitized = sanitizeOptionRecord(steps?.[index]);
|
|
92
|
+
next.push(sanitized);
|
|
93
|
+
if (sanitized) {
|
|
94
|
+
any = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return any ? next : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve per-step options for a transform chain.
|
|
102
|
+
* Prefers `steps`; otherwise expands shared `transformOptions` to every step (apply-to-all).
|
|
103
|
+
*/
|
|
104
|
+
export function resolveOptionSteps(
|
|
105
|
+
transformIds: readonly string[],
|
|
106
|
+
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
107
|
+
shared: Readonly<Record<string, unknown>> | undefined,
|
|
108
|
+
): (Readonly<Record<string, unknown>> | undefined)[] {
|
|
109
|
+
const length = transformIds.length;
|
|
110
|
+
if (length === 0) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
if (steps && steps.length > 0) {
|
|
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);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function resizeOptionSteps(
|
|
176
|
+
steps: readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined,
|
|
177
|
+
length: number,
|
|
178
|
+
): readonly (Readonly<Record<string, unknown>> | undefined)[] | undefined {
|
|
179
|
+
if (length <= 0) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const next = Array.from({ length }, (_, index) => sanitizeOptionRecord(steps?.[index]));
|
|
183
|
+
return sanitizeOptionSteps(next, length);
|
|
184
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import type { SourceField, TargetSlot } from '../types.js';
|
|
2
|
-
|
|
3
|
-
/** Flatten nested source fields depth-first. */
|
|
4
|
-
export function flattenSourceFields(fields: readonly SourceField[]): SourceField[] {
|
|
5
|
-
const out: SourceField[] = [];
|
|
6
|
-
for (const field of fields) {
|
|
7
|
-
out.push(field);
|
|
8
|
-
if (field.children?.length) {
|
|
9
|
-
out.push(...flattenSourceFields(field.children));
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
return out;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Flatten nested target slots depth-first. */
|
|
16
|
-
export function flattenTargetSlots(slots: readonly TargetSlot[]): TargetSlot[] {
|
|
17
|
-
const out: TargetSlot[] = [];
|
|
18
|
-
for (const slot of slots) {
|
|
19
|
-
out.push(slot);
|
|
20
|
-
if (slot.children?.length) {
|
|
21
|
-
out.push(...flattenTargetSlots(slot.children));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return out;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function findTargetSlot(
|
|
28
|
-
slots: readonly TargetSlot[],
|
|
29
|
-
slotId: string,
|
|
30
|
-
): TargetSlot | undefined {
|
|
31
|
-
return flattenTargetSlots(slots).find((slot) => slot.id === slotId);
|
|
32
|
-
}
|
|
1
|
+
import type { SourceField, TargetSlot } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/** Flatten nested source fields depth-first. */
|
|
4
|
+
export function flattenSourceFields(fields: readonly SourceField[]): SourceField[] {
|
|
5
|
+
const out: SourceField[] = [];
|
|
6
|
+
for (const field of fields) {
|
|
7
|
+
out.push(field);
|
|
8
|
+
if (field.children?.length) {
|
|
9
|
+
out.push(...flattenSourceFields(field.children));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Flatten nested target slots depth-first. */
|
|
16
|
+
export function flattenTargetSlots(slots: readonly TargetSlot[]): TargetSlot[] {
|
|
17
|
+
const out: TargetSlot[] = [];
|
|
18
|
+
for (const slot of slots) {
|
|
19
|
+
out.push(slot);
|
|
20
|
+
if (slot.children?.length) {
|
|
21
|
+
out.push(...flattenTargetSlots(slot.children));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function findTargetSlot(
|
|
28
|
+
slots: readonly TargetSlot[],
|
|
29
|
+
slotId: string,
|
|
30
|
+
): TargetSlot | undefined {
|
|
31
|
+
return flattenTargetSlots(slots).find((slot) => slot.id === slotId);
|
|
32
|
+
}
|