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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,194 +1,235 @@
1
- import { canonicalizeTransformId, MAX_TRANSFORM_CHAIN } from '../domain/constants.js';
2
- import type {
3
- FieldDataType,
4
- TransformContext,
5
- ValueTransformDefinition,
6
- ValueTransformListFilter,
7
- ValueTransformRegistry,
8
- } from '../domain/types.js';
9
-
10
- export function createValueTransformRegistry(
11
- initial: readonly ValueTransformDefinition[] = [],
12
- ): ValueTransformRegistry {
13
- const byId = new Map<string, ValueTransformDefinition>();
14
-
15
- for (const definition of initial) {
16
- byId.set(definition.id, definition);
17
- }
18
-
19
- function resolve(id: string): ValueTransformDefinition | undefined {
20
- const canonical = canonicalizeTransformId(id);
21
- return byId.get(canonical) ?? byId.get(id);
22
- }
23
-
24
- return {
25
- list(filter) {
26
- return [...byId.values()].filter((definition) => matchesFilter(definition, filter));
27
- },
28
- get(id) {
29
- return resolve(id);
30
- },
31
- apply(id, value, context = {}) {
32
- const definition = resolve(id);
33
- if (!definition) {
34
- throw new Error(`Unknown value transform: ${id}`);
35
- }
36
- return definition.apply(value, context);
37
- },
38
- register(definition) {
39
- byId.set(definition.id, definition);
40
- },
41
- };
42
- }
43
-
44
- /**
45
- * Apply an ordered transform chain (empty = identity / unchanged).
46
- * Optional `optionSteps[i]` merges over `context.options` for step `i` only.
47
- */
48
- export function applyTransformChain(
49
- registry: ValueTransformRegistry,
50
- transformIds: readonly string[],
51
- value: unknown,
52
- context: TransformContext = {},
53
- optionSteps?: readonly (Readonly<Record<string, unknown>> | undefined)[],
54
- ): unknown {
55
- let current = value;
56
- const ids = transformIds.slice(0, MAX_TRANSFORM_CHAIN);
57
- for (let index = 0; index < ids.length; index += 1) {
58
- const stepOptions = optionSteps?.[index];
59
- const stepContext =
60
- stepOptions && Object.keys(stepOptions).length > 0
61
- ? {
62
- ...context,
63
- options: {
64
- ...context.options,
65
- ...stepOptions,
66
- },
67
- }
68
- : context;
69
- current = registry.apply(ids[index]!, current, stepContext);
70
- }
71
- return current;
72
- }
73
-
74
- function matchesFilter(
75
- definition: ValueTransformDefinition,
76
- filter: ValueTransformListFilter | undefined,
77
- ): boolean {
78
- if (!filter) {
79
- return true;
80
- }
81
-
82
- if (filter.category && definition.category !== filter.category) {
83
- return false;
84
- }
85
-
86
- if (filter.inputType) {
87
- const inputTypes = definition.inputTypes;
88
- if (inputTypes && inputTypes.length > 0 && !inputTypes.includes(filter.inputType)) {
89
- return false;
90
- }
91
- }
92
-
93
- return true;
94
- }
95
-
96
- /** Whether a transform may sit between a typed source field and a typed target slot. */
97
- export function isTransformCompatible(
98
- definition: ValueTransformDefinition,
99
- sourceType: FieldDataType | undefined,
100
- targetType: FieldDataType | undefined,
101
- ): boolean {
102
- if (
103
- sourceType &&
104
- sourceType !== 'unknown' &&
105
- definition.inputTypes &&
106
- definition.inputTypes.length > 0 &&
107
- !definition.inputTypes.includes(sourceType)
108
- ) {
109
- return false;
110
- }
111
-
112
- // Collection sources only accept transforms that declare array input (or untyped).
113
- if (sourceType === 'array') {
114
- const acceptsArray =
115
- !definition.inputTypes ||
116
- definition.inputTypes.length === 0 ||
117
- definition.inputTypes.includes('array');
118
- if (!acceptsArray) {
119
- return false;
120
- }
121
- }
122
-
123
- // Array → array is pass-through only; reduce / typed outputs are not collection-preserving.
124
- if (
125
- sourceType === 'array' &&
126
- targetType === 'array' &&
127
- definition.outputType &&
128
- definition.outputType !== 'array'
129
- ) {
130
- return false;
131
- }
132
-
133
- if (
134
- targetType &&
135
- targetType !== 'unknown' &&
136
- definition.outputType &&
137
- definition.outputType !== 'unknown' &&
138
- definition.outputType !== targetType
139
- ) {
140
- // Formatted outputs are strings; string sinks accept them.
141
- return targetType === 'string';
142
- }
143
-
144
- // Pass-through / undeclared output: known source and target types must match.
145
- if (
146
- !definition.outputType &&
147
- sourceType &&
148
- sourceType !== 'unknown' &&
149
- targetType &&
150
- targetType !== 'unknown' &&
151
- sourceType !== targetType
152
- ) {
153
- return false;
154
- }
155
-
156
- return true;
157
- }
158
-
159
- /**
160
- * Whether an ordered transform chain can sit between source and target types.
161
- * Each step must accept the previous output type (or the original source for step 0).
162
- */
163
- export function isTransformChainCompatible(
164
- registry: ValueTransformRegistry,
165
- transformIds: readonly string[],
166
- sourceType: FieldDataType | undefined,
167
- targetType: FieldDataType | undefined,
168
- ): boolean {
169
- if (transformIds.length === 0) {
170
- return true;
171
- }
172
- if (transformIds.length > MAX_TRANSFORM_CHAIN) {
173
- return false;
174
- }
175
-
176
- let currentType = sourceType;
177
- for (let index = 0; index < transformIds.length; index += 1) {
178
- const definition = registry.get(transformIds[index]!);
179
- if (!definition) {
180
- return false;
181
- }
182
- const isLast = index === transformIds.length - 1;
183
- if (!isTransformCompatible(definition, currentType, isLast ? targetType : undefined)) {
184
- return false;
185
- }
186
- if (definition.outputType) {
187
- currentType = definition.outputType;
188
- }
189
- }
190
-
191
- return true;
192
- }
193
-
194
- export type { TransformContext };
1
+ import { throwIfAborted } from '../domain/abort.js';
2
+ import { canonicalizeTransformId, MAX_TRANSFORM_CHAIN } from '../domain/constants.js';
3
+ import type {
4
+ FieldDataType,
5
+ TransformContext,
6
+ ValueTransformDefinition,
7
+ ValueTransformListFilter,
8
+ ValueTransformRegistry,
9
+ } from '../domain/types.js';
10
+
11
+ export function createValueTransformRegistry(
12
+ initial: readonly ValueTransformDefinition[] = [],
13
+ ): ValueTransformRegistry {
14
+ const byId = new Map<string, ValueTransformDefinition>();
15
+
16
+ for (const definition of initial) {
17
+ byId.set(definition.id, definition);
18
+ }
19
+
20
+ function resolve(id: string): ValueTransformDefinition | undefined {
21
+ const canonical = canonicalizeTransformId(id);
22
+ return byId.get(canonical) ?? byId.get(id);
23
+ }
24
+
25
+ return {
26
+ list(filter) {
27
+ return [...byId.values()].filter((definition) => matchesFilter(definition, filter));
28
+ },
29
+ get(id) {
30
+ return resolve(id);
31
+ },
32
+ apply(id, value, context = {}) {
33
+ const definition = resolve(id);
34
+ if (!definition) {
35
+ throw new Error(`Unknown value transform: ${id}`);
36
+ }
37
+ return definition.apply(value, context);
38
+ },
39
+ register(definition) {
40
+ byId.set(definition.id, definition);
41
+ },
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Apply an ordered transform chain (empty = identity / unchanged).
47
+ * Optional `optionSteps[i]` merges over `context.options` for step `i` only.
48
+ * Awaits Promise-returning host transforms (e.g. JSONata 2.x).
49
+ */
50
+ export async function applyTransformChain(
51
+ registry: ValueTransformRegistry,
52
+ transformIds: readonly string[],
53
+ value: unknown,
54
+ context: TransformContext = {},
55
+ optionSteps?: readonly (Readonly<Record<string, unknown>> | undefined)[],
56
+ ): Promise<unknown> {
57
+ let current = value;
58
+ const ids = transformIds.slice(0, MAX_TRANSFORM_CHAIN);
59
+ for (let index = 0; index < ids.length; index += 1) {
60
+ throwIfAborted(context.signal);
61
+ const stepOptions = optionSteps?.[index];
62
+ const stepContext =
63
+ stepOptions && Object.keys(stepOptions).length > 0
64
+ ? {
65
+ ...context,
66
+ options: {
67
+ ...context.options,
68
+ ...stepOptions,
69
+ },
70
+ }
71
+ : context;
72
+ current = await registry.apply(ids[index]!, current, stepContext);
73
+ }
74
+ return current;
75
+ }
76
+
77
+ function matchesFilter(
78
+ definition: ValueTransformDefinition,
79
+ filter: ValueTransformListFilter | undefined,
80
+ ): boolean {
81
+ if (!filter) {
82
+ return true;
83
+ }
84
+
85
+ if (filter.category && definition.category !== filter.category) {
86
+ return false;
87
+ }
88
+
89
+ if (filter.inputType) {
90
+ const inputTypes = definition.inputTypes;
91
+ if (inputTypes && inputTypes.length > 0 && !inputTypes.includes(filter.inputType)) {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ return true;
97
+ }
98
+
99
+ /** Whether a transform may sit between a typed source field and a typed target slot. */
100
+ export function isTransformCompatible(
101
+ definition: ValueTransformDefinition,
102
+ sourceType: FieldDataType | undefined,
103
+ targetType: FieldDataType | undefined,
104
+ ): boolean {
105
+ if (
106
+ sourceType &&
107
+ sourceType !== 'unknown' &&
108
+ definition.inputTypes &&
109
+ definition.inputTypes.length > 0 &&
110
+ !definition.inputTypes.includes(sourceType)
111
+ ) {
112
+ return false;
113
+ }
114
+
115
+ // Collection sources only accept transforms that declare array input (or untyped).
116
+ if (sourceType === 'array') {
117
+ const acceptsArray =
118
+ !definition.inputTypes ||
119
+ definition.inputTypes.length === 0 ||
120
+ definition.inputTypes.includes('array');
121
+ if (!acceptsArray) {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ // Array array is pass-through only; reduce / typed outputs are not collection-preserving.
127
+ if (
128
+ sourceType === 'array' &&
129
+ targetType === 'array' &&
130
+ definition.outputType &&
131
+ definition.outputType !== 'array'
132
+ ) {
133
+ return false;
134
+ }
135
+
136
+ if (
137
+ targetType &&
138
+ targetType !== 'unknown' &&
139
+ definition.outputType &&
140
+ definition.outputType !== 'unknown' &&
141
+ definition.outputType !== targetType
142
+ ) {
143
+ // Formatted outputs are strings; string sinks accept them.
144
+ return targetType === 'string';
145
+ }
146
+
147
+ // Pass-through / undeclared output: known source and target types must match.
148
+ if (
149
+ !definition.outputType &&
150
+ sourceType &&
151
+ sourceType !== 'unknown' &&
152
+ targetType &&
153
+ targetType !== 'unknown' &&
154
+ sourceType !== targetType
155
+ ) {
156
+ return false;
157
+ }
158
+
159
+ return true;
160
+ }
161
+
162
+ /**
163
+ * Whether an ordered transform chain can sit between source and target types.
164
+ * Each step must accept the previous output type (or the original source for step 0).
165
+ * Empty chains always return true — use {@link arePortsCompatible} for identity type match.
166
+ */
167
+ export function isTransformChainCompatible(
168
+ registry: ValueTransformRegistry,
169
+ transformIds: readonly string[],
170
+ sourceType: FieldDataType | undefined,
171
+ targetType: FieldDataType | undefined,
172
+ ): boolean {
173
+ if (transformIds.length === 0) {
174
+ return true;
175
+ }
176
+ if (transformIds.length > MAX_TRANSFORM_CHAIN) {
177
+ return false;
178
+ }
179
+
180
+ let currentType = sourceType;
181
+ for (let index = 0; index < transformIds.length; index += 1) {
182
+ const definition = registry.get(transformIds[index]!);
183
+ if (!definition) {
184
+ return false;
185
+ }
186
+ const isLast = index === transformIds.length - 1;
187
+ if (!isTransformCompatible(definition, currentType, isLast ? targetType : undefined)) {
188
+ return false;
189
+ }
190
+ if (definition.outputType) {
191
+ currentType = definition.outputType;
192
+ }
193
+ }
194
+
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Identity (direct) port type match.
200
+ * Missing or `unknown` types are permissive — same default as transform helpers.
201
+ */
202
+ export function areFieldTypesCompatible(
203
+ source: FieldDataType | undefined,
204
+ target: FieldDataType | undefined,
205
+ ): boolean {
206
+ if (!source || source === 'unknown' || !target || target === 'unknown') {
207
+ return true;
208
+ }
209
+ return source === target;
210
+ }
211
+
212
+ export type ArePortsCompatibleInput = {
213
+ readonly sourceType?: FieldDataType;
214
+ readonly targetType?: FieldDataType;
215
+ readonly transformIds?: readonly string[];
216
+ readonly registry?: ValueTransformRegistry;
217
+ };
218
+
219
+ /**
220
+ * Whether two field/slot ports may connect under `FieldDataType` rules.
221
+ * Empty / omitted `transformIds` ≡ identity match via {@link areFieldTypesCompatible}.
222
+ * Non-empty chains require `registry` and use {@link isTransformChainCompatible}.
223
+ */
224
+ export function arePortsCompatible(input: ArePortsCompatibleInput): boolean {
225
+ const chain = input.transformIds ?? [];
226
+ if (chain.length === 0) {
227
+ return areFieldTypesCompatible(input.sourceType, input.targetType);
228
+ }
229
+ if (!input.registry) {
230
+ return false;
231
+ }
232
+ return isTransformChainCompatible(input.registry, chain, input.sourceType, input.targetType);
233
+ }
234
+
235
+ export type { TransformContext };