@workbench-kit/field-remap 0.0.2-prototype.0.2.5 → 0.0.2-prototype.0.2.8
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 -2
- package/package.json +1 -1
- package/src/domain/abort.ts +29 -0
- package/src/domain/mapping/convertItemEdges.ts +44 -39
- package/src/domain/mapping/objectPathSafety.ts +51 -0
- package/src/domain/mapping/pathUtils.ts +3 -14
- package/src/domain/mapping/resolveMappedValue.ts +16 -14
- package/src/domain/shapes/convertToShape.ts +25 -7
- package/src/domain/types.ts +11 -2
- package/src/index.ts +3 -1
- package/src/registry/createValueTransformRegistry.ts +6 -3
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ const conversion = defineConversion({
|
|
|
87
87
|
],
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
const { output } = convertToShape({
|
|
90
|
+
const { output } = await convertToShape({
|
|
91
91
|
conversion,
|
|
92
92
|
shapes,
|
|
93
93
|
inputs: { a: structureA },
|
|
@@ -97,7 +97,17 @@ const { output } = convertToShape({
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Hosts may `registry.register()` additional transforms (the sample registers `expr:jsonata` via
|
|
100
|
-
[jsonata](https://jsonata.org/)).
|
|
100
|
+
[jsonata](https://jsonata.org/)). `convertToShape` / `applyTransformChain` are async so Promise-returning
|
|
101
|
+
host transforms (JSONata 2.x) resolve correctly.
|
|
102
|
+
|
|
103
|
+
### Cancellation
|
|
104
|
+
|
|
105
|
+
Pass `signal` on `convertToShape` (or `TransformContext.signal`) to cancel stale previews.
|
|
106
|
+
Aborted runs reject with `AbortError` and stop further edges / chain steps. The shell Field Remap
|
|
107
|
+
panel wires an `AbortController` to effect cleanup.
|
|
108
|
+
|
|
109
|
+
Host JSONata transforms in `@workbench-kit/shell-react` are bounded by default (`timeoutMs`,
|
|
110
|
+
`maxExpressionLength`, `onError: 'throw'`). Use `createJsonataValueTransform()` to override.
|
|
101
111
|
|
|
102
112
|
## Layout
|
|
103
113
|
|
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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { throwIfAborted } from '../abort.js';
|
|
1
2
|
import { edgeTransformIds } from '../document/mappingEdge.js';
|
|
2
3
|
import { applyTransformChain } from '../../registry/createValueTransformRegistry.js';
|
|
3
4
|
import { BUILTIN_TRANSFORM_IDS } from '../../registry/builtinTransforms.js';
|
|
@@ -33,61 +34,65 @@ function itemRelativePath(
|
|
|
33
34
|
* Convert each object in a source array through list-context `itemEdges`.
|
|
34
35
|
* Child field paths are treated as item-relative (ingest array children).
|
|
35
36
|
*/
|
|
36
|
-
export function convertArrayWithItemEdges(input: {
|
|
37
|
+
export async function convertArrayWithItemEdges(input: {
|
|
37
38
|
readonly items: unknown;
|
|
38
39
|
readonly itemEdges: readonly MappingEdge[];
|
|
39
40
|
readonly sources: readonly SourceField[];
|
|
40
41
|
readonly targets: readonly TargetSlot[];
|
|
41
42
|
readonly transforms: ValueTransformRegistry;
|
|
42
43
|
readonly context?: TransformContext;
|
|
43
|
-
}): unknown[] {
|
|
44
|
+
}): Promise<unknown[]> {
|
|
44
45
|
if (!Array.isArray(input.items)) {
|
|
45
46
|
return [];
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const targetLeaves = flattenTargetSlots(input.targets);
|
|
49
50
|
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
return Promise.all(
|
|
52
|
+
input.items.map(async (rawItem) => {
|
|
53
|
+
const item = isPlainObject(rawItem) ? rawItem : {};
|
|
54
|
+
let outItem: Record<string, unknown> = {};
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const targetSlot =
|
|
57
|
-
findTargetSlot(input.targets, edge.targetSlotId) ??
|
|
58
|
-
targetLeaves.find((slot) => slot.id === edge.targetSlotId);
|
|
59
|
-
if (!sourceField || !targetSlot) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
56
|
+
for (const edge of input.itemEdges) {
|
|
57
|
+
throwIfAborted(input.context?.signal);
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
const sourceField = findSourceField(input.sources, edge.sourceFieldId);
|
|
60
|
+
const targetSlot =
|
|
61
|
+
findTargetSlot(input.targets, edge.targetSlotId) ??
|
|
62
|
+
targetLeaves.find((slot) => slot.id === edge.targetSlotId);
|
|
63
|
+
if (!sourceField || !targetSlot) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
value
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
chain,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
const sourcePath = itemRelativePath(sourceField);
|
|
68
|
+
const sourceValue = sourcePath ? readObjectPath(item, sourcePath) : undefined;
|
|
69
|
+
|
|
70
|
+
const chain = edgeTransformIds(edge);
|
|
71
|
+
let value: unknown;
|
|
72
|
+
if (chain.length === 0) {
|
|
73
|
+
const identity = input.transforms.get(BUILTIN_TRANSFORM_IDS.identity);
|
|
74
|
+
value = identity
|
|
75
|
+
? await identity.apply(sourceValue, { ...input.context, sampleValue: sourceValue })
|
|
76
|
+
: sourceValue;
|
|
77
|
+
} else {
|
|
78
|
+
const steps = resolveOptionSteps(chain, edge.transformOptionSteps, edge.transformOptions);
|
|
79
|
+
value = await applyTransformChain(
|
|
80
|
+
input.transforms,
|
|
81
|
+
chain,
|
|
82
|
+
sourceValue,
|
|
83
|
+
{ ...input.context, sampleValue: sourceValue },
|
|
84
|
+
steps,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
const targetPath = itemRelativePath(targetSlot);
|
|
89
|
+
if (!targetPath) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
outItem = writeObjectPath(outItem, targetPath, value);
|
|
87
93
|
}
|
|
88
|
-
outItem = writeObjectPath(outItem, targetPath, value);
|
|
89
|
-
}
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
return outItem;
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
93
98
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object-path safety gate for field-remap read/write and template placeholders.
|
|
3
|
+
* Keeps prototype-mutating segments out of dotted path traversal.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Identifier or dotted path: `city`, `a.b` (no expressions / eval). */
|
|
7
|
+
const SAFE_PATH_RE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
8
|
+
|
|
9
|
+
const UNSAFE_OBJECT_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
10
|
+
|
|
11
|
+
export class UnsafeObjectPathError extends Error {
|
|
12
|
+
readonly code = 'unsafe_object_path' as const;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly segment: string;
|
|
15
|
+
|
|
16
|
+
constructor(path: string, segment: string) {
|
|
17
|
+
super(`Object path "${path}" contains unsafe segment "${segment}".`);
|
|
18
|
+
this.name = 'UnsafeObjectPathError';
|
|
19
|
+
this.path = path;
|
|
20
|
+
this.segment = segment;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function objectPathParts(path: string): string[] {
|
|
25
|
+
return path
|
|
26
|
+
.split('.')
|
|
27
|
+
.map((part) => part.trim())
|
|
28
|
+
.filter((part) => part.length > 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findUnsafeObjectPathSegment(parts: readonly string[]): string | undefined {
|
|
32
|
+
return parts.find((part) => UNSAFE_OBJECT_PATH_SEGMENTS.has(part));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Parse dotted path segments and reject unsafe segments when any parts exist. */
|
|
36
|
+
export function requireObjectPathParts(path: string): string[] {
|
|
37
|
+
const parts = objectPathParts(path);
|
|
38
|
+
const unsafeSegment = findUnsafeObjectPathSegment(parts);
|
|
39
|
+
if (unsafeSegment) {
|
|
40
|
+
throw new UnsafeObjectPathError(path, unsafeSegment);
|
|
41
|
+
}
|
|
42
|
+
return parts;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isSafeObjectPath(path: string): boolean {
|
|
46
|
+
const trimmed = path.trim();
|
|
47
|
+
return (
|
|
48
|
+
SAFE_PATH_RE.test(trimmed) &&
|
|
49
|
+
findUnsafeObjectPathSegment(objectPathParts(trimmed)) === undefined
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* Supports simple dotted paths (`name`, `meta.label`) on plain objects.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const SAFE_PATH_RE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
6
|
+
import { isSafeObjectPath, requireObjectPathParts } from './objectPathSafety.js';
|
|
8
7
|
|
|
9
8
|
/** Placeholder matcher: `{city}`, `{a.b}` — rejects expressions / spaces. */
|
|
10
9
|
const TEMPLATE_PLACEHOLDER_RE = /\{([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\}/g;
|
|
@@ -13,10 +12,6 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
|
|
|
13
12
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export function isSafeObjectPath(path: string): boolean {
|
|
17
|
-
return SAFE_PATH_RE.test(path.trim());
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
/**
|
|
21
16
|
* Fill `{path}` placeholders from a plain object using safe dotted paths only.
|
|
22
17
|
* Unknown / unsafe placeholders become empty strings. No JS eval.
|
|
@@ -45,10 +40,7 @@ export function applyStringTemplate(
|
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
export function readObjectPath(value: unknown, path: string): unknown {
|
|
48
|
-
const parts = path
|
|
49
|
-
.split('.')
|
|
50
|
-
.map((part) => part.trim())
|
|
51
|
-
.filter((part) => part.length > 0);
|
|
43
|
+
const parts = requireObjectPathParts(path);
|
|
52
44
|
if (parts.length === 0) {
|
|
53
45
|
return value;
|
|
54
46
|
}
|
|
@@ -72,10 +64,7 @@ export function writeObjectPath(
|
|
|
72
64
|
path: string,
|
|
73
65
|
value: unknown,
|
|
74
66
|
): Record<string, unknown> {
|
|
75
|
-
const parts = path
|
|
76
|
-
.split('.')
|
|
77
|
-
.map((part) => part.trim())
|
|
78
|
-
.filter((part) => part.length > 0);
|
|
67
|
+
const parts = requireObjectPathParts(path);
|
|
79
68
|
if (parts.length === 0) {
|
|
80
69
|
return isPlainObject(root) ? { ...root } : {};
|
|
81
70
|
}
|
|
@@ -33,12 +33,12 @@ export function findSourceField(
|
|
|
33
33
|
* Per-step options (`transformOptionSteps` / `itemTransformOptionSteps`) win when
|
|
34
34
|
* present; otherwise shared `transformOptions` / `itemTransformOptions` apply to all steps.
|
|
35
35
|
*/
|
|
36
|
-
export function resolveMappedValue(
|
|
36
|
+
export async function resolveMappedValue(
|
|
37
37
|
edge: MappingEdge,
|
|
38
38
|
sourceValue: unknown,
|
|
39
39
|
registry: ValueTransformRegistry,
|
|
40
40
|
context: TransformContext = {},
|
|
41
|
-
): unknown {
|
|
41
|
+
): Promise<unknown> {
|
|
42
42
|
let current = edge.itemSourcePath
|
|
43
43
|
? projectCollectionItems(sourceValue, edge.itemSourcePath)
|
|
44
44
|
: sourceValue;
|
|
@@ -51,27 +51,27 @@ export function resolveMappedValue(
|
|
|
51
51
|
);
|
|
52
52
|
|
|
53
53
|
if (itemChain.length > 0 && Array.isArray(current)) {
|
|
54
|
-
current =
|
|
55
|
-
applyTransformChain(registry, itemChain, item, context, itemSteps),
|
|
54
|
+
current = await Promise.all(
|
|
55
|
+
current.map((item) => applyTransformChain(registry, itemChain, item, context, itemSteps)),
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const chain = edgeTransformIds(edge);
|
|
60
60
|
if (chain.length === 0) {
|
|
61
61
|
const identity = registry.get(BUILTIN_TRANSFORM_IDS.identity);
|
|
62
|
-
return identity ? identity.apply(current, context) : current;
|
|
62
|
+
return identity ? await identity.apply(current, context) : current;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const valueSteps = resolveOptionSteps(chain, edge.transformOptionSteps, edge.transformOptions);
|
|
66
66
|
return applyTransformChain(registry, chain, current, context, valueSteps);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export function resolveEdgePreview(
|
|
69
|
+
export async function resolveEdgePreview(
|
|
70
70
|
edge: MappingEdge,
|
|
71
71
|
sources: readonly SourceField[],
|
|
72
72
|
registry: ValueTransformRegistry,
|
|
73
73
|
context: TransformContext = {},
|
|
74
|
-
): unknown {
|
|
74
|
+
): Promise<unknown> {
|
|
75
75
|
const field = findSourceField(sources, edge.sourceFieldId);
|
|
76
76
|
const sample =
|
|
77
77
|
context.sampleValue !== undefined ? context.sampleValue : (field?.sampleValue ?? context.now);
|
|
@@ -82,15 +82,17 @@ export function resolveEdgePreview(
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/** Resolve every edge into `{ targetSlotId, value }` for live preview panels. */
|
|
85
|
-
export function resolveAllEdgePreviews(
|
|
85
|
+
export async function resolveAllEdgePreviews(
|
|
86
86
|
edges: readonly MappingEdge[],
|
|
87
87
|
sources: readonly SourceField[],
|
|
88
88
|
registry: ValueTransformRegistry,
|
|
89
89
|
context: TransformContext = {},
|
|
90
|
-
): ReadonlyArray<{ edgeId: string; targetSlotId: string; value: unknown }
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
);
|
|
96
98
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { throwIfAborted } from '../abort.js';
|
|
1
2
|
import type { ConversionDefinition } from './conversionDefinition.js';
|
|
2
3
|
import {
|
|
3
4
|
mergeSourceShapes,
|
|
@@ -27,6 +28,12 @@ export interface ConvertToShapeInput {
|
|
|
27
28
|
readonly inputs: Readonly<Record<string, unknown>>;
|
|
28
29
|
readonly transforms: ValueTransformRegistry;
|
|
29
30
|
readonly context?: TransformContext;
|
|
31
|
+
/**
|
|
32
|
+
* Optional cancellation. Merged into transform context as `signal` (wins over
|
|
33
|
+
* `context.signal` when both are set). Aborted conversions reject with `AbortError`
|
|
34
|
+
* and do not apply further edges.
|
|
35
|
+
*/
|
|
36
|
+
readonly signal?: AbortSignal;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
export interface ConvertToShapeSlotResult {
|
|
@@ -112,8 +119,17 @@ function readFieldValue(field: SourceField, inputs: Readonly<Record<string, unkn
|
|
|
112
119
|
*
|
|
113
120
|
* This is the host runtime entry point — not `sourceShape.convert(target, data)`.
|
|
114
121
|
* Multiple source shapes are supported via `inputs[shapeId]`.
|
|
122
|
+
* Awaits Promise-returning host transforms (e.g. JSONata 2.x).
|
|
115
123
|
*/
|
|
116
|
-
export function convertToShape(input: ConvertToShapeInput): ConvertToShapeResult {
|
|
124
|
+
export async function convertToShape(input: ConvertToShapeInput): Promise<ConvertToShapeResult> {
|
|
125
|
+
const signal = input.signal ?? input.context?.signal;
|
|
126
|
+
const context: TransformContext = {
|
|
127
|
+
...input.context,
|
|
128
|
+
...(signal ? { signal } : {}),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
throwIfAborted(signal);
|
|
132
|
+
|
|
117
133
|
const sourceShapes: DataShape[] = [];
|
|
118
134
|
for (const shapeId of input.conversion.sourceShapeIds) {
|
|
119
135
|
const shape = resolveShape(input.shapes, shapeId);
|
|
@@ -141,6 +157,8 @@ export function convertToShape(input: ConvertToShapeInput): ConvertToShapeResult
|
|
|
141
157
|
const slots: ConvertToShapeSlotResult[] = [];
|
|
142
158
|
|
|
143
159
|
for (const edge of input.conversion.document.edges) {
|
|
160
|
+
throwIfAborted(signal);
|
|
161
|
+
|
|
144
162
|
const sourceField = findSourceField(sources, edge.sourceFieldId);
|
|
145
163
|
if (!sourceField) {
|
|
146
164
|
continue;
|
|
@@ -155,21 +173,21 @@ export function convertToShape(input: ConvertToShapeInput): ConvertToShapeResult
|
|
|
155
173
|
const sourceValue = readFieldValue(sourceField, input.inputs);
|
|
156
174
|
const value =
|
|
157
175
|
edge.itemEdges && edge.itemEdges.length > 0
|
|
158
|
-
? convertArrayWithItemEdges({
|
|
176
|
+
? await convertArrayWithItemEdges({
|
|
159
177
|
items: sourceValue,
|
|
160
178
|
itemEdges: edge.itemEdges,
|
|
161
179
|
sources,
|
|
162
180
|
targets,
|
|
163
181
|
transforms: input.transforms,
|
|
164
|
-
context
|
|
182
|
+
context,
|
|
165
183
|
})
|
|
166
|
-
: resolveMappedValue(edge, sourceValue, input.transforms, {
|
|
167
|
-
...
|
|
184
|
+
: await resolveMappedValue(edge, sourceValue, input.transforms, {
|
|
185
|
+
...context,
|
|
168
186
|
sampleValue: sourceValue,
|
|
169
187
|
record: isPlainObject(sourceValue)
|
|
170
188
|
? sourceValue
|
|
171
|
-
: isPlainObject(
|
|
172
|
-
?
|
|
189
|
+
: isPlainObject(context.record)
|
|
190
|
+
? context.record
|
|
173
191
|
: undefined,
|
|
174
192
|
});
|
|
175
193
|
|
package/src/domain/types.ts
CHANGED
|
@@ -134,6 +134,11 @@ export interface TransformContext {
|
|
|
134
134
|
*/
|
|
135
135
|
readonly record?: Readonly<Record<string, unknown>>;
|
|
136
136
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
137
|
+
/**
|
|
138
|
+
* Optional cancellation signal. `applyTransformChain` / `convertToShape` check
|
|
139
|
+
* between steps and reject with `AbortError` when aborted.
|
|
140
|
+
*/
|
|
141
|
+
readonly signal?: AbortSignal;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
/** Declares a host-editable option consumed via `context.options[key]`. */
|
|
@@ -157,7 +162,11 @@ export interface ValueTransformDefinition {
|
|
|
157
162
|
readonly category?: string;
|
|
158
163
|
readonly inputTypes?: readonly FieldDataType[];
|
|
159
164
|
readonly outputType?: FieldDataType;
|
|
160
|
-
|
|
165
|
+
/**
|
|
166
|
+
* May return a Promise (e.g. host JSONata 2.x). Prefer `applyTransformChain` /
|
|
167
|
+
* `convertToShape`, which always await transform results.
|
|
168
|
+
*/
|
|
169
|
+
readonly apply: (value: unknown, context: TransformContext) => unknown | PromiseLike<unknown>;
|
|
161
170
|
/** Optional picker label that includes a live format sample. */
|
|
162
171
|
readonly formatSampleLabel?: (context: TransformContext) => string;
|
|
163
172
|
/**
|
|
@@ -177,6 +186,6 @@ export interface ValueTransformListFilter {
|
|
|
177
186
|
export interface ValueTransformRegistry {
|
|
178
187
|
list(filter?: ValueTransformListFilter): ValueTransformDefinition[];
|
|
179
188
|
get(id: string): ValueTransformDefinition | undefined;
|
|
180
|
-
apply(id: string, value: unknown, context?: TransformContext): unknown
|
|
189
|
+
apply(id: string, value: unknown, context?: TransformContext): unknown | PromiseLike<unknown>;
|
|
181
190
|
register(definition: ValueTransformDefinition): void;
|
|
182
191
|
}
|
package/src/index.ts
CHANGED
|
@@ -38,10 +38,10 @@ export {
|
|
|
38
38
|
UnsupportedFieldRemapDocumentVersionError,
|
|
39
39
|
} from './domain/document/fieldRemapDocument.js';
|
|
40
40
|
|
|
41
|
+
export { isSafeObjectPath, UnsafeObjectPathError } from './domain/mapping/objectPathSafety.js';
|
|
41
42
|
export {
|
|
42
43
|
applyStringTemplate,
|
|
43
44
|
isPlainObject,
|
|
44
|
-
isSafeObjectPath,
|
|
45
45
|
listArrayItemProjectionOptions,
|
|
46
46
|
projectCollectionItems,
|
|
47
47
|
readObjectPath,
|
|
@@ -112,6 +112,8 @@ export type {
|
|
|
112
112
|
ConvertToShapeSlotResult,
|
|
113
113
|
} from './domain/shapes/convertToShape.js';
|
|
114
114
|
|
|
115
|
+
export { createAbortError, isAbortError, throwIfAborted } from './domain/abort.js';
|
|
116
|
+
|
|
115
117
|
export { sourceFieldsFromPlainObject } from './domain/ingest/sourceFieldsFromPlainObject.js';
|
|
116
118
|
export type { SourceFieldsFromPlainObjectOptions } from './domain/ingest/sourceFieldsFromPlainObject.js';
|
|
117
119
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { throwIfAborted } from '../domain/abort.js';
|
|
1
2
|
import { canonicalizeTransformId, MAX_TRANSFORM_CHAIN } from '../domain/constants.js';
|
|
2
3
|
import type {
|
|
3
4
|
FieldDataType,
|
|
@@ -44,17 +45,19 @@ export function createValueTransformRegistry(
|
|
|
44
45
|
/**
|
|
45
46
|
* Apply an ordered transform chain (empty = identity / unchanged).
|
|
46
47
|
* Optional `optionSteps[i]` merges over `context.options` for step `i` only.
|
|
48
|
+
* Awaits Promise-returning host transforms (e.g. JSONata 2.x).
|
|
47
49
|
*/
|
|
48
|
-
export function applyTransformChain(
|
|
50
|
+
export async function applyTransformChain(
|
|
49
51
|
registry: ValueTransformRegistry,
|
|
50
52
|
transformIds: readonly string[],
|
|
51
53
|
value: unknown,
|
|
52
54
|
context: TransformContext = {},
|
|
53
55
|
optionSteps?: readonly (Readonly<Record<string, unknown>> | undefined)[],
|
|
54
|
-
): unknown {
|
|
56
|
+
): Promise<unknown> {
|
|
55
57
|
let current = value;
|
|
56
58
|
const ids = transformIds.slice(0, MAX_TRANSFORM_CHAIN);
|
|
57
59
|
for (let index = 0; index < ids.length; index += 1) {
|
|
60
|
+
throwIfAborted(context.signal);
|
|
58
61
|
const stepOptions = optionSteps?.[index];
|
|
59
62
|
const stepContext =
|
|
60
63
|
stepOptions && Object.keys(stepOptions).length > 0
|
|
@@ -66,7 +69,7 @@ export function applyTransformChain(
|
|
|
66
69
|
},
|
|
67
70
|
}
|
|
68
71
|
: context;
|
|
69
|
-
current = registry.apply(ids[index]!, current, stepContext);
|
|
72
|
+
current = await registry.apply(ids[index]!, current, stepContext);
|
|
70
73
|
}
|
|
71
74
|
return current;
|
|
72
75
|
}
|