@workbench-kit/field-remap 0.0.1-prototype.0

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/src/index.ts ADDED
@@ -0,0 +1,137 @@
1
+ export type {
2
+ FieldDataType,
3
+ MappingEdge,
4
+ FieldRemapDocument,
5
+ SourceField,
6
+ TargetSlot,
7
+ TransformContext,
8
+ TransformOptionField,
9
+ ValueTransformDefinition,
10
+ ValueTransformListFilter,
11
+ ValueTransformRegistry,
12
+ } from './domain/types.js';
13
+
14
+ export {
15
+ canonicalizeTransformId,
16
+ IDENTITY_TRANSFORM_ID,
17
+ MAX_TRANSFORM_CHAIN,
18
+ TRANSFORM_ID_ALIASES,
19
+ } from './domain/constants.js';
20
+
21
+ export {
22
+ createMappingEdge,
23
+ edgeItemTransformIds,
24
+ edgeTransformIds,
25
+ normalizeMappingEdge,
26
+ normalizeMappingEdges,
27
+ } from './domain/document/mappingEdge.js';
28
+
29
+ export {
30
+ createFieldRemapDocument,
31
+ deserializeFieldRemapDocument,
32
+ InvalidFieldRemapDocumentError,
33
+ migrateFieldRemapDocument,
34
+ normalizeFieldRemapDocument,
35
+ parseFieldRemapDocument,
36
+ FIELD_REMAP_DOCUMENT_VERSION,
37
+ serializeFieldRemapDocument,
38
+ UnsupportedFieldRemapDocumentVersionError,
39
+ } from './domain/document/fieldRemapDocument.js';
40
+
41
+ export {
42
+ applyStringTemplate,
43
+ isPlainObject,
44
+ isSafeObjectPath,
45
+ listArrayItemProjectionOptions,
46
+ projectCollectionItems,
47
+ readObjectPath,
48
+ writeObjectPath,
49
+ } from './domain/mapping/pathUtils.js';
50
+ export type { ArrayItemProjectionOption } from './domain/mapping/pathUtils.js';
51
+
52
+ export {
53
+ formatDateParts,
54
+ parseDateParts,
55
+ reformatDateString,
56
+ splitDateTimeString,
57
+ } from './domain/mapping/dateFormat.js';
58
+ export type { DateParts } from './domain/mapping/dateFormat.js';
59
+
60
+ export { convertArrayWithItemEdges } from './domain/mapping/convertItemEdges.js';
61
+ export { findParentChildMappingConflicts } from './domain/mapping/mappingConflicts.js';
62
+ export type { MappingConflict } from './domain/mapping/mappingConflicts.js';
63
+
64
+ export {
65
+ collectOptionFields,
66
+ contextWithEdgeOptions,
67
+ mergeOptionSteps,
68
+ optionFieldsForStep,
69
+ patchOptionRecord,
70
+ patchOptionStep,
71
+ resolveOptionSteps,
72
+ resizeOptionSteps,
73
+ sanitizeOptionRecord,
74
+ sanitizeOptionSteps,
75
+ sharedOptionsFromSteps,
76
+ } from './domain/mapping/transformOptions.js';
77
+
78
+ export {
79
+ findSourceField,
80
+ findTargetSlot,
81
+ flattenSourceFields,
82
+ flattenTargetSlots,
83
+ resolveAllEdgePreviews,
84
+ resolveEdgePreview,
85
+ resolveMappedValue,
86
+ } from './domain/mapping/resolveMappedValue.js';
87
+
88
+ export {
89
+ attachShapeIdToSourceFields,
90
+ createDataShapeRegistry,
91
+ defineDataShape,
92
+ mergeSourceShapes,
93
+ targetSlotsFromShape,
94
+ } from './domain/shapes/dataShape.js';
95
+ export type { DataShape, DataShapeRegistry, DataShapeRole } from './domain/shapes/dataShape.js';
96
+
97
+ export {
98
+ createConversionRegistry,
99
+ defineConversion,
100
+ withConversionEdges,
101
+ } from './domain/shapes/conversionDefinition.js';
102
+ export type {
103
+ ConversionDefinition,
104
+ ConversionRegistry,
105
+ DefineConversionInput,
106
+ } from './domain/shapes/conversionDefinition.js';
107
+
108
+ export { convertToShape } from './domain/shapes/convertToShape.js';
109
+ export type {
110
+ ConvertToShapeInput,
111
+ ConvertToShapeResult,
112
+ ConvertToShapeSlotResult,
113
+ } from './domain/shapes/convertToShape.js';
114
+
115
+ export { sourceFieldsFromPlainObject } from './domain/ingest/sourceFieldsFromPlainObject.js';
116
+ export type { SourceFieldsFromPlainObjectOptions } from './domain/ingest/sourceFieldsFromPlainObject.js';
117
+
118
+ export { targetSlotsFromPlainObject } from './domain/ingest/targetSlotsFromPlainObject.js';
119
+ export type { TargetSlotsFromPlainObjectOptions } from './domain/ingest/targetSlotsFromPlainObject.js';
120
+
121
+ export {
122
+ applyTransformChain,
123
+ createValueTransformRegistry,
124
+ isTransformChainCompatible,
125
+ isTransformCompatible,
126
+ } from './registry/createValueTransformRegistry.js';
127
+
128
+ export {
129
+ ARRAY_REDUCE_TRANSFORM_IDS,
130
+ BUILTIN_TRANSFORM_IDS,
131
+ DATE_STYLE_TRANSFORM_IDS,
132
+ STRING_FORMAT_TRANSFORM_IDS,
133
+ TIME_FORMAT_TRANSFORM_IDS,
134
+ builtinValueTransforms,
135
+ createBuiltinValueTransformRegistry,
136
+ } from './registry/builtinTransforms.js';
137
+ export type { CreateBuiltinValueTransformRegistryOptions } from './registry/builtinTransforms.js';
@@ -0,0 +1,243 @@
1
+ import { reformatDateString, splitDateTimeString } from '../domain/mapping/dateFormat.js';
2
+ import { applyStringTemplate, isPlainObject } from '../domain/mapping/pathUtils.js';
3
+ import type { ValueTransformDefinition } from '../domain/types.js';
4
+ import { createValueTransformRegistry } from './createValueTransformRegistry.js';
5
+
6
+ export const BUILTIN_TRANSFORM_IDS = {
7
+ identity: 'identity',
8
+ arrayFirst: 'array:first',
9
+ arrayJoin: 'array:join',
10
+ stringTrim: 'string:trim',
11
+ stringUpper: 'string:upper',
12
+ stringLower: 'string:lower',
13
+ stringPrefix: 'string:prefix',
14
+ stringSuffix: 'string:suffix',
15
+ stringTemplate: 'string:template',
16
+ dateReformat: 'date:reformat',
17
+ datetimeCombine: 'datetime:combine',
18
+ datetimeDate: 'datetime:date',
19
+ datetimeTime: 'datetime:time',
20
+ } as const;
21
+
22
+ export const ARRAY_REDUCE_TRANSFORM_IDS = [
23
+ BUILTIN_TRANSFORM_IDS.arrayFirst,
24
+ BUILTIN_TRANSFORM_IDS.arrayJoin,
25
+ ] as const;
26
+
27
+ export const STRING_FORMAT_TRANSFORM_IDS = [
28
+ BUILTIN_TRANSFORM_IDS.stringTrim,
29
+ BUILTIN_TRANSFORM_IDS.stringUpper,
30
+ BUILTIN_TRANSFORM_IDS.stringLower,
31
+ BUILTIN_TRANSFORM_IDS.stringPrefix,
32
+ BUILTIN_TRANSFORM_IDS.stringSuffix,
33
+ ] as const;
34
+
35
+ function asString(value: unknown): string {
36
+ if (value === null || value === undefined) {
37
+ return '';
38
+ }
39
+ return typeof value === 'string' ? value : String(value);
40
+ }
41
+
42
+ export const builtinValueTransforms: readonly ValueTransformDefinition[] = [
43
+ {
44
+ id: BUILTIN_TRANSFORM_IDS.identity,
45
+ label: 'Pass-through',
46
+ description: 'Return the source value unchanged.',
47
+ category: 'utility',
48
+ inputTypes: [
49
+ 'string',
50
+ 'number',
51
+ 'boolean',
52
+ 'date',
53
+ 'time',
54
+ 'datetime',
55
+ 'object',
56
+ 'array',
57
+ 'unknown',
58
+ ],
59
+ outputType: 'unknown',
60
+ apply: (value) => value,
61
+ },
62
+ {
63
+ id: BUILTIN_TRANSFORM_IDS.arrayFirst,
64
+ label: 'Array first',
65
+ description: 'Return the first element of an array (or the value when not an array).',
66
+ category: 'array',
67
+ inputTypes: ['array', 'unknown'],
68
+ outputType: 'unknown',
69
+ apply: (value) => (Array.isArray(value) ? value[0] : value),
70
+ },
71
+ {
72
+ id: BUILTIN_TRANSFORM_IDS.arrayJoin,
73
+ label: 'Array join',
74
+ description: 'Join array elements with a separator (default ", ").',
75
+ category: 'array',
76
+ inputTypes: ['array', 'unknown'],
77
+ outputType: 'string',
78
+ optionFields: [
79
+ {
80
+ key: 'separator',
81
+ label: 'Separator',
82
+ kind: 'string',
83
+ },
84
+ ],
85
+ apply: (value, context) => {
86
+ if (!Array.isArray(value)) {
87
+ return asString(value);
88
+ }
89
+ const separator =
90
+ typeof context.options?.separator === 'string' ? context.options.separator : ', ';
91
+ return value.map((item) => asString(item)).join(separator);
92
+ },
93
+ },
94
+ {
95
+ id: BUILTIN_TRANSFORM_IDS.stringTrim,
96
+ label: 'Trim',
97
+ description: 'Trim leading and trailing whitespace.',
98
+ category: 'string',
99
+ inputTypes: ['string', 'number', 'unknown'],
100
+ outputType: 'string',
101
+ apply: (value) => asString(value).trim(),
102
+ },
103
+ {
104
+ id: BUILTIN_TRANSFORM_IDS.stringUpper,
105
+ label: 'Uppercase',
106
+ description: 'Convert text to uppercase.',
107
+ category: 'string',
108
+ inputTypes: ['string', 'number', 'unknown'],
109
+ outputType: 'string',
110
+ apply: (value) => asString(value).toUpperCase(),
111
+ },
112
+ {
113
+ id: BUILTIN_TRANSFORM_IDS.stringLower,
114
+ label: 'Lowercase',
115
+ description: 'Convert text to lowercase.',
116
+ category: 'string',
117
+ inputTypes: ['string', 'number', 'unknown'],
118
+ outputType: 'string',
119
+ apply: (value) => asString(value).toLowerCase(),
120
+ },
121
+ {
122
+ id: BUILTIN_TRANSFORM_IDS.stringPrefix,
123
+ label: 'Prefix',
124
+ description: 'Prepend a fixed string.',
125
+ category: 'string',
126
+ inputTypes: ['string', 'number', 'unknown'],
127
+ outputType: 'string',
128
+ optionFields: [{ key: 'value', label: 'Prefix', kind: 'string' }],
129
+ apply: (value, context) => {
130
+ const prefix = typeof context.options?.value === 'string' ? context.options.value : '';
131
+ return `${prefix}${asString(value)}`;
132
+ },
133
+ },
134
+ {
135
+ id: BUILTIN_TRANSFORM_IDS.stringSuffix,
136
+ label: 'Suffix',
137
+ description: 'Append a fixed string.',
138
+ category: 'string',
139
+ inputTypes: ['string', 'number', 'unknown'],
140
+ outputType: 'string',
141
+ optionFields: [{ key: 'value', label: 'Suffix', kind: 'string' }],
142
+ apply: (value, context) => {
143
+ const suffix = typeof context.options?.value === 'string' ? context.options.value : '';
144
+ return `${asString(value)}${suffix}`;
145
+ },
146
+ },
147
+ {
148
+ id: BUILTIN_TRANSFORM_IDS.stringTemplate,
149
+ label: 'Template',
150
+ description: 'Fill {path} placeholders from an object (e.g. "{first} {last}").',
151
+ category: 'string',
152
+ inputTypes: ['object', 'unknown'],
153
+ outputType: 'string',
154
+ optionFields: [{ key: 'template', label: 'Template', kind: 'string' }],
155
+ apply: (value, context) => {
156
+ const template =
157
+ typeof context.options?.template === 'string' ? context.options.template : '';
158
+ if (!template) {
159
+ return isPlainObject(value) ? JSON.stringify(value) : asString(value);
160
+ }
161
+ return applyStringTemplate(template, isPlainObject(value) ? value : undefined);
162
+ },
163
+ },
164
+ {
165
+ id: BUILTIN_TRANSFORM_IDS.dateReformat,
166
+ label: 'Date reformat',
167
+ description: 'Reformat a date string (token formats: YYYY, MM, DD).',
168
+ category: 'date',
169
+ inputTypes: ['string', 'unknown'],
170
+ outputType: 'string',
171
+ optionFields: [
172
+ { key: 'inputFormat', label: 'Input format', kind: 'string' },
173
+ { key: 'outputFormat', label: 'Output format', kind: 'string' },
174
+ ],
175
+ apply: (value, context) => {
176
+ const inputFormat =
177
+ typeof context.options?.inputFormat === 'string' ? context.options.inputFormat : 'YYYYMMDD';
178
+ const outputFormat =
179
+ typeof context.options?.outputFormat === 'string'
180
+ ? context.options.outputFormat
181
+ : 'YYYY-MM-DD';
182
+ const text = asString(value);
183
+ return reformatDateString(text, inputFormat, outputFormat) ?? text;
184
+ },
185
+ },
186
+ {
187
+ id: BUILTIN_TRANSFORM_IDS.datetimeCombine,
188
+ label: 'Combine date+time',
189
+ description: 'Join object fields into a datetime string (default keys: date, time).',
190
+ category: 'date',
191
+ inputTypes: ['object', 'unknown'],
192
+ outputType: 'datetime',
193
+ optionFields: [
194
+ { key: 'dateKey', label: 'Date key', kind: 'string' },
195
+ { key: 'timeKey', label: 'Time key', kind: 'string' },
196
+ { key: 'separator', label: 'Separator', kind: 'string' },
197
+ ],
198
+ apply: (value, context) => {
199
+ if (!isPlainObject(value)) {
200
+ return asString(value);
201
+ }
202
+ const dateKey =
203
+ typeof context.options?.dateKey === 'string' ? context.options.dateKey : 'date';
204
+ const timeKey =
205
+ typeof context.options?.timeKey === 'string' ? context.options.timeKey : 'time';
206
+ const separator =
207
+ typeof context.options?.separator === 'string' ? context.options.separator : 'T';
208
+ return `${asString(value[dateKey])}${separator}${asString(value[timeKey])}`;
209
+ },
210
+ },
211
+ {
212
+ id: BUILTIN_TRANSFORM_IDS.datetimeDate,
213
+ label: 'Date part',
214
+ description: 'Take the date part from a datetime string.',
215
+ category: 'date',
216
+ inputTypes: ['string', 'datetime', 'unknown'],
217
+ outputType: 'date',
218
+ apply: (value) => splitDateTimeString(asString(value))?.date ?? asString(value),
219
+ },
220
+ {
221
+ id: BUILTIN_TRANSFORM_IDS.datetimeTime,
222
+ label: 'Time part',
223
+ description: 'Take the time part from a datetime string.',
224
+ category: 'date',
225
+ inputTypes: ['string', 'datetime', 'unknown'],
226
+ outputType: 'time',
227
+ apply: (value) => splitDateTimeString(asString(value))?.time ?? '',
228
+ },
229
+ ];
230
+
231
+ export type CreateBuiltinValueTransformRegistryOptions = Record<string, never>;
232
+
233
+ /** Builtin registry. Hosts may `register()` additional transforms. */
234
+ export function createBuiltinValueTransformRegistry(
235
+ _options?: CreateBuiltinValueTransformRegistryOptions,
236
+ ) {
237
+ return createValueTransformRegistry(builtinValueTransforms);
238
+ }
239
+
240
+ /** @deprecated Empty — kept for import stability while hosts migrate. */
241
+ export const TIME_FORMAT_TRANSFORM_IDS = [] as const;
242
+ /** @deprecated Empty — kept for import stability while hosts migrate. */
243
+ export const DATE_STYLE_TRANSFORM_IDS = [] as const;
@@ -0,0 +1,194 @@
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 };