@workbench-kit/field-remap 0.0.1-prototype.0 → 0.0.2-prototype.0.2.4
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 +115 -115
- package/package.json +1 -1
- package/src/domain/constants.ts +14 -14
- package/src/domain/document/fieldRemapDocument.ts +129 -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 +93 -93
- package/src/domain/mapping/dateFormat.ts +102 -102
- package/src/domain/mapping/mappingConflicts.ts +90 -90
- package/src/domain/mapping/pathUtils.ts +156 -156
- package/src/domain/mapping/resolveMappedValue.ts +96 -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/convertToShape.ts +187 -187
- package/src/domain/shapes/dataShape.ts +109 -109
- package/src/domain/types.ts +182 -182
- package/src/index.ts +137 -137
- package/src/registry/builtinTransforms.ts +243 -243
- package/src/registry/createValueTransformRegistry.ts +194 -194
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { edgeTransformIds } from '../document/mappingEdge.js';
|
|
2
|
-
import { applyTransformChain } from '../../registry/createValueTransformRegistry.js';
|
|
3
|
-
import { BUILTIN_TRANSFORM_IDS } from '../../registry/builtinTransforms.js';
|
|
4
|
-
import { resolveOptionSteps } from './transformOptions.js';
|
|
5
|
-
import { isPlainObject, readObjectPath, writeObjectPath } from './pathUtils.js';
|
|
6
|
-
import { findTargetSlot, flattenSourceFields, flattenTargetSlots } from './treeUtils.js';
|
|
7
|
-
import type {
|
|
8
|
-
MappingEdge,
|
|
9
|
-
SourceField,
|
|
10
|
-
TargetSlot,
|
|
11
|
-
TransformContext,
|
|
12
|
-
ValueTransformRegistry,
|
|
13
|
-
} from '../types.js';
|
|
14
|
-
|
|
15
|
-
function findSourceField(fields: readonly SourceField[], fieldId: string): SourceField | undefined {
|
|
16
|
-
return flattenSourceFields(fields).find((field) => field.id === fieldId);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function itemRelativePath(
|
|
20
|
-
node: { readonly path?: string; readonly label: string; readonly id: string } | undefined,
|
|
21
|
-
): string {
|
|
22
|
-
if (!node) {
|
|
23
|
-
return '';
|
|
24
|
-
}
|
|
25
|
-
const path = node.path?.trim();
|
|
26
|
-
if (path) {
|
|
27
|
-
return path;
|
|
28
|
-
}
|
|
29
|
-
return node.label.trim() || node.id.split('.').pop() || '';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Convert each object in a source array through list-context `itemEdges`.
|
|
34
|
-
* Child field paths are treated as item-relative (ingest array children).
|
|
35
|
-
*/
|
|
36
|
-
export function convertArrayWithItemEdges(input: {
|
|
37
|
-
readonly items: unknown;
|
|
38
|
-
readonly itemEdges: readonly MappingEdge[];
|
|
39
|
-
readonly sources: readonly SourceField[];
|
|
40
|
-
readonly targets: readonly TargetSlot[];
|
|
41
|
-
readonly transforms: ValueTransformRegistry;
|
|
42
|
-
readonly context?: TransformContext;
|
|
43
|
-
}): unknown[] {
|
|
44
|
-
if (!Array.isArray(input.items)) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const targetLeaves = flattenTargetSlots(input.targets);
|
|
49
|
-
|
|
50
|
-
return input.items.map((rawItem) => {
|
|
51
|
-
const item = isPlainObject(rawItem) ? rawItem : {};
|
|
52
|
-
let outItem: Record<string, unknown> = {};
|
|
53
|
-
|
|
54
|
-
for (const edge of input.itemEdges) {
|
|
55
|
-
const sourceField = findSourceField(input.sources, edge.sourceFieldId);
|
|
56
|
-
const targetSlot =
|
|
57
|
-
findTargetSlot(input.targets, edge.targetSlotId) ??
|
|
58
|
-
targetLeaves.find((slot) => slot.id === edge.targetSlotId);
|
|
59
|
-
if (!sourceField || !targetSlot) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const sourcePath = itemRelativePath(sourceField);
|
|
64
|
-
const sourceValue = sourcePath ? readObjectPath(item, sourcePath) : undefined;
|
|
65
|
-
|
|
66
|
-
const chain = edgeTransformIds(edge);
|
|
67
|
-
let value: unknown;
|
|
68
|
-
if (chain.length === 0) {
|
|
69
|
-
const identity = input.transforms.get(BUILTIN_TRANSFORM_IDS.identity);
|
|
70
|
-
value = identity
|
|
71
|
-
? identity.apply(sourceValue, { ...input.context, sampleValue: sourceValue })
|
|
72
|
-
: sourceValue;
|
|
73
|
-
} else {
|
|
74
|
-
const steps = resolveOptionSteps(chain, edge.transformOptionSteps, edge.transformOptions);
|
|
75
|
-
value = applyTransformChain(
|
|
76
|
-
input.transforms,
|
|
77
|
-
chain,
|
|
78
|
-
sourceValue,
|
|
79
|
-
{ ...input.context, sampleValue: sourceValue },
|
|
80
|
-
steps,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const targetPath = itemRelativePath(targetSlot);
|
|
85
|
-
if (!targetPath) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
outItem = writeObjectPath(outItem, targetPath, value);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return outItem;
|
|
92
|
-
});
|
|
93
|
-
}
|
|
1
|
+
import { edgeTransformIds } from '../document/mappingEdge.js';
|
|
2
|
+
import { applyTransformChain } from '../../registry/createValueTransformRegistry.js';
|
|
3
|
+
import { BUILTIN_TRANSFORM_IDS } from '../../registry/builtinTransforms.js';
|
|
4
|
+
import { resolveOptionSteps } from './transformOptions.js';
|
|
5
|
+
import { isPlainObject, readObjectPath, writeObjectPath } from './pathUtils.js';
|
|
6
|
+
import { findTargetSlot, flattenSourceFields, flattenTargetSlots } from './treeUtils.js';
|
|
7
|
+
import type {
|
|
8
|
+
MappingEdge,
|
|
9
|
+
SourceField,
|
|
10
|
+
TargetSlot,
|
|
11
|
+
TransformContext,
|
|
12
|
+
ValueTransformRegistry,
|
|
13
|
+
} from '../types.js';
|
|
14
|
+
|
|
15
|
+
function findSourceField(fields: readonly SourceField[], fieldId: string): SourceField | undefined {
|
|
16
|
+
return flattenSourceFields(fields).find((field) => field.id === fieldId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function itemRelativePath(
|
|
20
|
+
node: { readonly path?: string; readonly label: string; readonly id: string } | undefined,
|
|
21
|
+
): string {
|
|
22
|
+
if (!node) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
const path = node.path?.trim();
|
|
26
|
+
if (path) {
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
return node.label.trim() || node.id.split('.').pop() || '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert each object in a source array through list-context `itemEdges`.
|
|
34
|
+
* Child field paths are treated as item-relative (ingest array children).
|
|
35
|
+
*/
|
|
36
|
+
export function convertArrayWithItemEdges(input: {
|
|
37
|
+
readonly items: unknown;
|
|
38
|
+
readonly itemEdges: readonly MappingEdge[];
|
|
39
|
+
readonly sources: readonly SourceField[];
|
|
40
|
+
readonly targets: readonly TargetSlot[];
|
|
41
|
+
readonly transforms: ValueTransformRegistry;
|
|
42
|
+
readonly context?: TransformContext;
|
|
43
|
+
}): unknown[] {
|
|
44
|
+
if (!Array.isArray(input.items)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const targetLeaves = flattenTargetSlots(input.targets);
|
|
49
|
+
|
|
50
|
+
return input.items.map((rawItem) => {
|
|
51
|
+
const item = isPlainObject(rawItem) ? rawItem : {};
|
|
52
|
+
let outItem: Record<string, unknown> = {};
|
|
53
|
+
|
|
54
|
+
for (const edge of input.itemEdges) {
|
|
55
|
+
const sourceField = findSourceField(input.sources, edge.sourceFieldId);
|
|
56
|
+
const targetSlot =
|
|
57
|
+
findTargetSlot(input.targets, edge.targetSlotId) ??
|
|
58
|
+
targetLeaves.find((slot) => slot.id === edge.targetSlotId);
|
|
59
|
+
if (!sourceField || !targetSlot) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sourcePath = itemRelativePath(sourceField);
|
|
64
|
+
const sourceValue = sourcePath ? readObjectPath(item, sourcePath) : undefined;
|
|
65
|
+
|
|
66
|
+
const chain = edgeTransformIds(edge);
|
|
67
|
+
let value: unknown;
|
|
68
|
+
if (chain.length === 0) {
|
|
69
|
+
const identity = input.transforms.get(BUILTIN_TRANSFORM_IDS.identity);
|
|
70
|
+
value = identity
|
|
71
|
+
? identity.apply(sourceValue, { ...input.context, sampleValue: sourceValue })
|
|
72
|
+
: sourceValue;
|
|
73
|
+
} else {
|
|
74
|
+
const steps = resolveOptionSteps(chain, edge.transformOptionSteps, edge.transformOptions);
|
|
75
|
+
value = applyTransformChain(
|
|
76
|
+
input.transforms,
|
|
77
|
+
chain,
|
|
78
|
+
sourceValue,
|
|
79
|
+
{ ...input.context, sampleValue: sourceValue },
|
|
80
|
+
steps,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const targetPath = itemRelativePath(targetSlot);
|
|
85
|
+
if (!targetPath) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
outItem = writeObjectPath(outItem, targetPath, value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return outItem;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
export type DateParts = { readonly year: string; readonly month: string; readonly day: string };
|
|
2
|
-
|
|
3
|
-
const FORMAT_TOKEN_RE = /YYYY|MM|DD/g;
|
|
4
|
-
|
|
5
|
-
/** Parse a date string with a simple token format (YYYY/MM/DD only). */
|
|
6
|
-
export function parseDateParts(value: string, inputFormat: string): DateParts | undefined {
|
|
7
|
-
const format = inputFormat.trim();
|
|
8
|
-
const raw = value.trim();
|
|
9
|
-
if (!format || !raw) {
|
|
10
|
-
return undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let year = '';
|
|
14
|
-
let month = '';
|
|
15
|
-
let day = '';
|
|
16
|
-
let cursor = 0;
|
|
17
|
-
|
|
18
|
-
for (let i = 0; i < format.length;) {
|
|
19
|
-
if (format.startsWith('YYYY', i)) {
|
|
20
|
-
year = raw.slice(cursor, cursor + 4);
|
|
21
|
-
if (!/^\d{4}$/.test(year)) {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
cursor += 4;
|
|
25
|
-
i += 4;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (format.startsWith('MM', i)) {
|
|
29
|
-
month = raw.slice(cursor, cursor + 2);
|
|
30
|
-
if (!/^\d{2}$/.test(month)) {
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
cursor += 2;
|
|
34
|
-
i += 2;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (format.startsWith('DD', i)) {
|
|
38
|
-
day = raw.slice(cursor, cursor + 2);
|
|
39
|
-
if (!/^\d{2}$/.test(day)) {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
cursor += 2;
|
|
43
|
-
i += 2;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (raw[cursor] !== format[i]) {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
cursor += 1;
|
|
50
|
-
i += 1;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (cursor !== raw.length || !year || !month || !day) {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
return { year, month, day };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function formatDateParts(parts: DateParts, outputFormat: string): string {
|
|
60
|
-
return outputFormat.replace(FORMAT_TOKEN_RE, (token) => {
|
|
61
|
-
if (token === 'YYYY') {
|
|
62
|
-
return parts.year;
|
|
63
|
-
}
|
|
64
|
-
if (token === 'MM') {
|
|
65
|
-
return parts.month;
|
|
66
|
-
}
|
|
67
|
-
return parts.day;
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function reformatDateString(
|
|
72
|
-
value: string,
|
|
73
|
-
inputFormat: string,
|
|
74
|
-
outputFormat: string,
|
|
75
|
-
): string | undefined {
|
|
76
|
-
const parts = parseDateParts(value, inputFormat);
|
|
77
|
-
if (!parts) {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
return formatDateParts(parts, outputFormat);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Split `YYYY-MM-DDTHH:mm:ss` / `YYYY-MM-DD HH:mm:ss` / date-only. */
|
|
84
|
-
export function splitDateTimeString(value: string): { date: string; time: string } | undefined {
|
|
85
|
-
const raw = value.trim();
|
|
86
|
-
if (!raw) {
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
const tIndex = raw.indexOf('T');
|
|
90
|
-
const spaceIndex = raw.indexOf(' ');
|
|
91
|
-
const splitAt = tIndex >= 0 ? tIndex : spaceIndex;
|
|
92
|
-
if (splitAt < 0) {
|
|
93
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(raw) || /^\d{8}$/.test(raw)) {
|
|
94
|
-
return { date: raw, time: '' };
|
|
95
|
-
}
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
date: raw.slice(0, splitAt),
|
|
100
|
-
time: raw.slice(splitAt + 1),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
1
|
+
export type DateParts = { readonly year: string; readonly month: string; readonly day: string };
|
|
2
|
+
|
|
3
|
+
const FORMAT_TOKEN_RE = /YYYY|MM|DD/g;
|
|
4
|
+
|
|
5
|
+
/** Parse a date string with a simple token format (YYYY/MM/DD only). */
|
|
6
|
+
export function parseDateParts(value: string, inputFormat: string): DateParts | undefined {
|
|
7
|
+
const format = inputFormat.trim();
|
|
8
|
+
const raw = value.trim();
|
|
9
|
+
if (!format || !raw) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let year = '';
|
|
14
|
+
let month = '';
|
|
15
|
+
let day = '';
|
|
16
|
+
let cursor = 0;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < format.length;) {
|
|
19
|
+
if (format.startsWith('YYYY', i)) {
|
|
20
|
+
year = raw.slice(cursor, cursor + 4);
|
|
21
|
+
if (!/^\d{4}$/.test(year)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
cursor += 4;
|
|
25
|
+
i += 4;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (format.startsWith('MM', i)) {
|
|
29
|
+
month = raw.slice(cursor, cursor + 2);
|
|
30
|
+
if (!/^\d{2}$/.test(month)) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
cursor += 2;
|
|
34
|
+
i += 2;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (format.startsWith('DD', i)) {
|
|
38
|
+
day = raw.slice(cursor, cursor + 2);
|
|
39
|
+
if (!/^\d{2}$/.test(day)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
cursor += 2;
|
|
43
|
+
i += 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (raw[cursor] !== format[i]) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
cursor += 1;
|
|
50
|
+
i += 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (cursor !== raw.length || !year || !month || !day) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return { year, month, day };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatDateParts(parts: DateParts, outputFormat: string): string {
|
|
60
|
+
return outputFormat.replace(FORMAT_TOKEN_RE, (token) => {
|
|
61
|
+
if (token === 'YYYY') {
|
|
62
|
+
return parts.year;
|
|
63
|
+
}
|
|
64
|
+
if (token === 'MM') {
|
|
65
|
+
return parts.month;
|
|
66
|
+
}
|
|
67
|
+
return parts.day;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function reformatDateString(
|
|
72
|
+
value: string,
|
|
73
|
+
inputFormat: string,
|
|
74
|
+
outputFormat: string,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
const parts = parseDateParts(value, inputFormat);
|
|
77
|
+
if (!parts) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return formatDateParts(parts, outputFormat);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Split `YYYY-MM-DDTHH:mm:ss` / `YYYY-MM-DD HH:mm:ss` / date-only. */
|
|
84
|
+
export function splitDateTimeString(value: string): { date: string; time: string } | undefined {
|
|
85
|
+
const raw = value.trim();
|
|
86
|
+
if (!raw) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const tIndex = raw.indexOf('T');
|
|
90
|
+
const spaceIndex = raw.indexOf(' ');
|
|
91
|
+
const splitAt = tIndex >= 0 ? tIndex : spaceIndex;
|
|
92
|
+
if (splitAt < 0) {
|
|
93
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(raw) || /^\d{8}$/.test(raw)) {
|
|
94
|
+
return { date: raw, time: '' };
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
date: raw.slice(0, splitAt),
|
|
100
|
+
time: raw.slice(splitAt + 1),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import type { MappingEdge, SourceField, TargetSlot } from '../types.js';
|
|
2
|
-
import { flattenSourceFields, flattenTargetSlots } from './treeUtils.js';
|
|
3
|
-
|
|
4
|
-
export interface MappingConflict {
|
|
5
|
-
readonly kind: 'parent-child-source' | 'parent-child-target';
|
|
6
|
-
readonly parentId: string;
|
|
7
|
-
readonly childId: string;
|
|
8
|
-
readonly parentEdgeId: string;
|
|
9
|
-
readonly childEdgeId: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isAncestorId(ancestorId: string, descendantId: string): boolean {
|
|
13
|
-
return (
|
|
14
|
-
descendantId.startsWith(`${ancestorId}.`) || descendantId.startsWith(`${ancestorId}.item.`)
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Detect edges that map both a parent object/array and one of its descendants.
|
|
20
|
-
* Hosts should warn — writing both usually overwrites or double-defines output.
|
|
21
|
-
*/
|
|
22
|
-
export function findParentChildMappingConflicts(
|
|
23
|
-
edges: readonly MappingEdge[],
|
|
24
|
-
sources: readonly SourceField[],
|
|
25
|
-
targets: readonly TargetSlot[],
|
|
26
|
-
): MappingConflict[] {
|
|
27
|
-
const sourceIds = new Set(flattenSourceFields(sources).map((field) => field.id));
|
|
28
|
-
const targetIds = new Set(flattenTargetSlots(targets).map((slot) => slot.id));
|
|
29
|
-
const conflicts: MappingConflict[] = [];
|
|
30
|
-
|
|
31
|
-
for (let i = 0; i < edges.length; i += 1) {
|
|
32
|
-
const a = edges[i]!;
|
|
33
|
-
for (let j = i + 1; j < edges.length; j += 1) {
|
|
34
|
-
const b = edges[j]!;
|
|
35
|
-
if (
|
|
36
|
-
sourceIds.has(a.sourceFieldId) &&
|
|
37
|
-
sourceIds.has(b.sourceFieldId) &&
|
|
38
|
-
isAncestorId(a.sourceFieldId, b.sourceFieldId)
|
|
39
|
-
) {
|
|
40
|
-
conflicts.push({
|
|
41
|
-
kind: 'parent-child-source',
|
|
42
|
-
parentId: a.sourceFieldId,
|
|
43
|
-
childId: b.sourceFieldId,
|
|
44
|
-
parentEdgeId: a.id,
|
|
45
|
-
childEdgeId: b.id,
|
|
46
|
-
});
|
|
47
|
-
} else if (
|
|
48
|
-
sourceIds.has(a.sourceFieldId) &&
|
|
49
|
-
sourceIds.has(b.sourceFieldId) &&
|
|
50
|
-
isAncestorId(b.sourceFieldId, a.sourceFieldId)
|
|
51
|
-
) {
|
|
52
|
-
conflicts.push({
|
|
53
|
-
kind: 'parent-child-source',
|
|
54
|
-
parentId: b.sourceFieldId,
|
|
55
|
-
childId: a.sourceFieldId,
|
|
56
|
-
parentEdgeId: b.id,
|
|
57
|
-
childEdgeId: a.id,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
targetIds.has(a.targetSlotId) &&
|
|
63
|
-
targetIds.has(b.targetSlotId) &&
|
|
64
|
-
isAncestorId(a.targetSlotId, b.targetSlotId)
|
|
65
|
-
) {
|
|
66
|
-
conflicts.push({
|
|
67
|
-
kind: 'parent-child-target',
|
|
68
|
-
parentId: a.targetSlotId,
|
|
69
|
-
childId: b.targetSlotId,
|
|
70
|
-
parentEdgeId: a.id,
|
|
71
|
-
childEdgeId: b.id,
|
|
72
|
-
});
|
|
73
|
-
} else if (
|
|
74
|
-
targetIds.has(a.targetSlotId) &&
|
|
75
|
-
targetIds.has(b.targetSlotId) &&
|
|
76
|
-
isAncestorId(b.targetSlotId, a.targetSlotId)
|
|
77
|
-
) {
|
|
78
|
-
conflicts.push({
|
|
79
|
-
kind: 'parent-child-target',
|
|
80
|
-
parentId: b.targetSlotId,
|
|
81
|
-
childId: a.targetSlotId,
|
|
82
|
-
parentEdgeId: b.id,
|
|
83
|
-
childEdgeId: a.id,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return conflicts;
|
|
90
|
-
}
|
|
1
|
+
import type { MappingEdge, SourceField, TargetSlot } from '../types.js';
|
|
2
|
+
import { flattenSourceFields, flattenTargetSlots } from './treeUtils.js';
|
|
3
|
+
|
|
4
|
+
export interface MappingConflict {
|
|
5
|
+
readonly kind: 'parent-child-source' | 'parent-child-target';
|
|
6
|
+
readonly parentId: string;
|
|
7
|
+
readonly childId: string;
|
|
8
|
+
readonly parentEdgeId: string;
|
|
9
|
+
readonly childEdgeId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isAncestorId(ancestorId: string, descendantId: string): boolean {
|
|
13
|
+
return (
|
|
14
|
+
descendantId.startsWith(`${ancestorId}.`) || descendantId.startsWith(`${ancestorId}.item.`)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect edges that map both a parent object/array and one of its descendants.
|
|
20
|
+
* Hosts should warn — writing both usually overwrites or double-defines output.
|
|
21
|
+
*/
|
|
22
|
+
export function findParentChildMappingConflicts(
|
|
23
|
+
edges: readonly MappingEdge[],
|
|
24
|
+
sources: readonly SourceField[],
|
|
25
|
+
targets: readonly TargetSlot[],
|
|
26
|
+
): MappingConflict[] {
|
|
27
|
+
const sourceIds = new Set(flattenSourceFields(sources).map((field) => field.id));
|
|
28
|
+
const targetIds = new Set(flattenTargetSlots(targets).map((slot) => slot.id));
|
|
29
|
+
const conflicts: MappingConflict[] = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
32
|
+
const a = edges[i]!;
|
|
33
|
+
for (let j = i + 1; j < edges.length; j += 1) {
|
|
34
|
+
const b = edges[j]!;
|
|
35
|
+
if (
|
|
36
|
+
sourceIds.has(a.sourceFieldId) &&
|
|
37
|
+
sourceIds.has(b.sourceFieldId) &&
|
|
38
|
+
isAncestorId(a.sourceFieldId, b.sourceFieldId)
|
|
39
|
+
) {
|
|
40
|
+
conflicts.push({
|
|
41
|
+
kind: 'parent-child-source',
|
|
42
|
+
parentId: a.sourceFieldId,
|
|
43
|
+
childId: b.sourceFieldId,
|
|
44
|
+
parentEdgeId: a.id,
|
|
45
|
+
childEdgeId: b.id,
|
|
46
|
+
});
|
|
47
|
+
} else if (
|
|
48
|
+
sourceIds.has(a.sourceFieldId) &&
|
|
49
|
+
sourceIds.has(b.sourceFieldId) &&
|
|
50
|
+
isAncestorId(b.sourceFieldId, a.sourceFieldId)
|
|
51
|
+
) {
|
|
52
|
+
conflicts.push({
|
|
53
|
+
kind: 'parent-child-source',
|
|
54
|
+
parentId: b.sourceFieldId,
|
|
55
|
+
childId: a.sourceFieldId,
|
|
56
|
+
parentEdgeId: b.id,
|
|
57
|
+
childEdgeId: a.id,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
targetIds.has(a.targetSlotId) &&
|
|
63
|
+
targetIds.has(b.targetSlotId) &&
|
|
64
|
+
isAncestorId(a.targetSlotId, b.targetSlotId)
|
|
65
|
+
) {
|
|
66
|
+
conflicts.push({
|
|
67
|
+
kind: 'parent-child-target',
|
|
68
|
+
parentId: a.targetSlotId,
|
|
69
|
+
childId: b.targetSlotId,
|
|
70
|
+
parentEdgeId: a.id,
|
|
71
|
+
childEdgeId: b.id,
|
|
72
|
+
});
|
|
73
|
+
} else if (
|
|
74
|
+
targetIds.has(a.targetSlotId) &&
|
|
75
|
+
targetIds.has(b.targetSlotId) &&
|
|
76
|
+
isAncestorId(b.targetSlotId, a.targetSlotId)
|
|
77
|
+
) {
|
|
78
|
+
conflicts.push({
|
|
79
|
+
kind: 'parent-child-target',
|
|
80
|
+
parentId: b.targetSlotId,
|
|
81
|
+
childId: a.targetSlotId,
|
|
82
|
+
parentEdgeId: b.id,
|
|
83
|
+
childEdgeId: a.id,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return conflicts;
|
|
90
|
+
}
|