angular-data-mapper 1.0.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/.claude/settings.local.json +10 -0
- package/LICENSE +190 -0
- package/PUBLISHING.md +75 -0
- package/README.md +214 -0
- package/angular.json +121 -0
- package/package.json +67 -0
- package/projects/demo-app/public/favicon.ico +0 -0
- package/projects/demo-app/src/app/app.config.ts +12 -0
- package/projects/demo-app/src/app/app.html +36 -0
- package/projects/demo-app/src/app/app.routes.ts +62 -0
- package/projects/demo-app/src/app/app.scss +65 -0
- package/projects/demo-app/src/app/app.ts +11 -0
- package/projects/demo-app/src/app/layout/app-layout.component.ts +294 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.html +87 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.scss +202 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.ts +192 -0
- package/projects/demo-app/src/app/pages/mappings-page/add-mapping-dialog.component.ts +163 -0
- package/projects/demo-app/src/app/pages/mappings-page/mappings-page.component.ts +306 -0
- package/projects/demo-app/src/app/pages/schema-creator-page/schema-creator-page.component.ts +88 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.html +108 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.scss +317 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.ts +129 -0
- package/projects/demo-app/src/app/services/app-state.service.ts +233 -0
- package/projects/demo-app/src/app/services/sample-data.service.ts +228 -0
- package/projects/demo-app/src/index.html +15 -0
- package/projects/demo-app/src/main.ts +6 -0
- package/projects/demo-app/src/styles.scss +54 -0
- package/projects/demo-app/tsconfig.app.json +13 -0
- package/projects/ngx-data-mapper/ng-package.json +7 -0
- package/projects/ngx-data-mapper/package.json +40 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.html +183 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.scss +352 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.ts +277 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.html +174 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.scss +357 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.ts +258 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.html +139 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.scss +213 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.ts +261 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.html +199 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.scss +321 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.ts +618 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.html +67 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.scss +97 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.ts +105 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.html +552 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.scss +824 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.ts +730 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.html +82 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.scss +352 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.ts +225 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.html +346 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.scss +511 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.ts +368 -0
- package/projects/ngx-data-mapper/src/lib/models/json-schema.model.ts +164 -0
- package/projects/ngx-data-mapper/src/lib/models/schema.model.ts +173 -0
- package/projects/ngx-data-mapper/src/lib/services/mapping.service.ts +615 -0
- package/projects/ngx-data-mapper/src/lib/services/schema-parser.service.ts +270 -0
- package/projects/ngx-data-mapper/src/lib/services/svg-connector.service.ts +135 -0
- package/projects/ngx-data-mapper/src/lib/services/transformation.service.ts +453 -0
- package/projects/ngx-data-mapper/src/public-api.ts +22 -0
- package/projects/ngx-data-mapper/tsconfig.lib.json +13 -0
- package/projects/ngx-data-mapper/tsconfig.lib.prod.json +9 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { Injectable, signal, computed } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
FieldMapping,
|
|
4
|
+
ArrayMapping,
|
|
5
|
+
ArrayToObjectMapping,
|
|
6
|
+
ArraySelectorConfig,
|
|
7
|
+
SchemaField,
|
|
8
|
+
TransformationConfig,
|
|
9
|
+
Connection,
|
|
10
|
+
DragState,
|
|
11
|
+
ArrayFilterConfig,
|
|
12
|
+
DefaultValue,
|
|
13
|
+
} from '../models/schema.model';
|
|
14
|
+
|
|
15
|
+
@Injectable({
|
|
16
|
+
providedIn: 'root',
|
|
17
|
+
})
|
|
18
|
+
export class MappingService {
|
|
19
|
+
private mappings = signal<FieldMapping[]>([]);
|
|
20
|
+
private arrayMappings = signal<ArrayMapping[]>([]);
|
|
21
|
+
private arrayToObjectMappings = signal<ArrayToObjectMapping[]>([]);
|
|
22
|
+
private defaultValues = signal<DefaultValue[]>([]);
|
|
23
|
+
private selectedMappingId = signal<string | null>(null);
|
|
24
|
+
private _sourceSchemaRef = signal<string | null>(null);
|
|
25
|
+
private _targetSchemaRef = signal<string | null>(null);
|
|
26
|
+
private dragState = signal<DragState>({
|
|
27
|
+
isDragging: false,
|
|
28
|
+
sourceField: null,
|
|
29
|
+
startPoint: null,
|
|
30
|
+
currentPoint: null,
|
|
31
|
+
dragMode: 'new',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
readonly allMappings = computed(() => this.mappings());
|
|
35
|
+
readonly allArrayMappings = computed(() => this.arrayMappings());
|
|
36
|
+
readonly allArrayToObjectMappings = computed(() => this.arrayToObjectMappings());
|
|
37
|
+
readonly allDefaultValues = computed(() => this.defaultValues());
|
|
38
|
+
readonly sourceSchemaRef = computed(() => this._sourceSchemaRef());
|
|
39
|
+
readonly targetSchemaRef = computed(() => this._targetSchemaRef());
|
|
40
|
+
readonly selectedMapping = computed(() => {
|
|
41
|
+
const id = this.selectedMappingId();
|
|
42
|
+
return this.mappings().find((m) => m.id === id) || null;
|
|
43
|
+
});
|
|
44
|
+
readonly currentDragState = computed(() => this.dragState());
|
|
45
|
+
|
|
46
|
+
private generateId(): string {
|
|
47
|
+
return `mapping-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
startDrag(field: SchemaField, startPoint: { x: number; y: number }): void {
|
|
51
|
+
this.dragState.set({
|
|
52
|
+
isDragging: true,
|
|
53
|
+
sourceField: field,
|
|
54
|
+
startPoint,
|
|
55
|
+
currentPoint: startPoint,
|
|
56
|
+
dragMode: 'new',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
startEndpointDrag(
|
|
61
|
+
mappingId: string,
|
|
62
|
+
endpointType: 'source' | 'target',
|
|
63
|
+
startPoint: { x: number; y: number },
|
|
64
|
+
sourceFieldIndex?: number
|
|
65
|
+
): void {
|
|
66
|
+
const mapping = this.mappings().find(m => m.id === mappingId);
|
|
67
|
+
if (!mapping) return;
|
|
68
|
+
|
|
69
|
+
// For source endpoint, we show the line from target back to cursor
|
|
70
|
+
// For target endpoint, we show the line from source(s) to cursor
|
|
71
|
+
const dragMode = endpointType === 'source' ? 'move-source' : 'move-target';
|
|
72
|
+
|
|
73
|
+
this.dragState.set({
|
|
74
|
+
isDragging: true,
|
|
75
|
+
sourceField: endpointType === 'source'
|
|
76
|
+
? mapping.sourceFields[sourceFieldIndex ?? 0]
|
|
77
|
+
: mapping.targetField,
|
|
78
|
+
startPoint,
|
|
79
|
+
currentPoint: startPoint,
|
|
80
|
+
dragMode,
|
|
81
|
+
mappingId,
|
|
82
|
+
sourceFieldIndex,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
updateDragPosition(currentPoint: { x: number; y: number }): void {
|
|
87
|
+
this.dragState.update((state) => ({
|
|
88
|
+
...state,
|
|
89
|
+
currentPoint,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
endDrag(): void {
|
|
94
|
+
this.dragState.set({
|
|
95
|
+
isDragging: false,
|
|
96
|
+
sourceField: null,
|
|
97
|
+
startPoint: null,
|
|
98
|
+
currentPoint: null,
|
|
99
|
+
dragMode: 'new',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
changeSourceField(mappingId: string, newSourceField: SchemaField, sourceFieldIndex?: number): void {
|
|
104
|
+
const mapping = this.mappings().find(m => m.id === mappingId);
|
|
105
|
+
if (!mapping) return;
|
|
106
|
+
|
|
107
|
+
// Don't allow if new source is the same as target
|
|
108
|
+
if (newSourceField.id === mapping.targetField.id) return;
|
|
109
|
+
|
|
110
|
+
// Don't allow if source already exists in the mapping
|
|
111
|
+
if (mapping.sourceFields.some(sf => sf.id === newSourceField.id)) return;
|
|
112
|
+
|
|
113
|
+
if (sourceFieldIndex !== undefined && mapping.sourceFields.length > 1) {
|
|
114
|
+
// Replace specific source field in multi-source mapping
|
|
115
|
+
const newSourceFields = [...mapping.sourceFields];
|
|
116
|
+
newSourceFields[sourceFieldIndex] = newSourceField;
|
|
117
|
+
this.mappings.update(mappings =>
|
|
118
|
+
mappings.map(m => m.id === mappingId ? { ...m, sourceFields: newSourceFields } : m)
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
// Single source mapping - replace the source field
|
|
122
|
+
this.mappings.update(mappings =>
|
|
123
|
+
mappings.map(m => m.id === mappingId ? { ...m, sourceFields: [newSourceField] } : m)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
changeTargetField(mappingId: string, newTargetField: SchemaField): void {
|
|
129
|
+
const mapping = this.mappings().find(m => m.id === mappingId);
|
|
130
|
+
if (!mapping) return;
|
|
131
|
+
|
|
132
|
+
// Don't allow if new target is same as any source
|
|
133
|
+
if (mapping.sourceFields.some(sf => sf.id === newTargetField.id)) return;
|
|
134
|
+
|
|
135
|
+
// Check if another mapping already targets this field
|
|
136
|
+
const existingMapping = this.mappings().find(
|
|
137
|
+
m => m.targetField.id === newTargetField.id && m.id !== mappingId
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (existingMapping) {
|
|
141
|
+
// Merge into existing mapping (add sources)
|
|
142
|
+
const mergedSources = [
|
|
143
|
+
...existingMapping.sourceFields,
|
|
144
|
+
...mapping.sourceFields.filter(
|
|
145
|
+
sf => !existingMapping.sourceFields.some(esf => esf.id === sf.id)
|
|
146
|
+
),
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
this.mappings.update(mappings =>
|
|
150
|
+
mappings
|
|
151
|
+
.filter(m => m.id !== mappingId) // Remove old mapping
|
|
152
|
+
.map(m => m.id === existingMapping.id
|
|
153
|
+
? {
|
|
154
|
+
...m,
|
|
155
|
+
sourceFields: mergedSources,
|
|
156
|
+
transformations: mergedSources.length > 1
|
|
157
|
+
? [{ type: 'concat' as const, separator: ' ' }]
|
|
158
|
+
: m.transformations,
|
|
159
|
+
}
|
|
160
|
+
: m
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
} else {
|
|
164
|
+
// Simply update the target field
|
|
165
|
+
this.mappings.update(mappings =>
|
|
166
|
+
mappings.map(m => m.id === mappingId ? { ...m, targetField: newTargetField } : m)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
createMapping(
|
|
172
|
+
sourceFields: SchemaField[],
|
|
173
|
+
targetField: SchemaField,
|
|
174
|
+
transformation?: TransformationConfig
|
|
175
|
+
): FieldMapping {
|
|
176
|
+
// Check if this is an array-to-array mapping
|
|
177
|
+
const sourceField = sourceFields[0];
|
|
178
|
+
if (sourceField.type === 'array' && targetField.type === 'array') {
|
|
179
|
+
return this.createArrayMapping(sourceField, targetField);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if this is an array-to-object mapping
|
|
183
|
+
if (sourceField.type === 'array' && targetField.type === 'object') {
|
|
184
|
+
return this.createArrayToObjectMapping(sourceField, targetField);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if fields are within arrays and need array context
|
|
188
|
+
const arrayMappingId = this.findOrCreateArrayContext(sourceField, targetField);
|
|
189
|
+
|
|
190
|
+
// Check if fields need array-to-object context
|
|
191
|
+
const arrayToObjectMappingId = this.findOrCreateArrayToObjectContext(sourceField, targetField);
|
|
192
|
+
|
|
193
|
+
const existingMapping = this.mappings().find(
|
|
194
|
+
(m) => m.targetField.id === targetField.id
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (existingMapping) {
|
|
198
|
+
// Add source fields to existing mapping (for concat scenarios)
|
|
199
|
+
const updatedMapping: FieldMapping = {
|
|
200
|
+
...existingMapping,
|
|
201
|
+
sourceFields: [
|
|
202
|
+
...existingMapping.sourceFields,
|
|
203
|
+
...sourceFields.filter(
|
|
204
|
+
(sf) => !existingMapping.sourceFields.some((esf) => esf.id === sf.id)
|
|
205
|
+
),
|
|
206
|
+
],
|
|
207
|
+
transformations:
|
|
208
|
+
existingMapping.sourceFields.length + sourceFields.length > 1
|
|
209
|
+
? [{ type: 'concat', separator: ' ', ...transformation }]
|
|
210
|
+
: transformation ? [transformation] : [{ type: 'direct' }],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
this.mappings.update((mappings) =>
|
|
214
|
+
mappings.map((m) => (m.id === existingMapping.id ? updatedMapping : m))
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return updatedMapping;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const newMapping: FieldMapping = {
|
|
221
|
+
id: this.generateId(),
|
|
222
|
+
sourceFields,
|
|
223
|
+
targetField,
|
|
224
|
+
transformations: transformation ? [transformation] : [{ type: 'direct' }],
|
|
225
|
+
isArrayMapping: false, // Only true for array-to-array connections
|
|
226
|
+
arrayMappingId, // Links to parent array mapping if within array context
|
|
227
|
+
isArrayToObjectMapping: false,
|
|
228
|
+
arrayToObjectMappingId, // Links to parent array-to-object mapping if applicable
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
this.mappings.update((mappings) => [...mappings, newMapping]);
|
|
232
|
+
|
|
233
|
+
// If this is part of an array mapping, add to itemMappings
|
|
234
|
+
if (arrayMappingId) {
|
|
235
|
+
this.arrayMappings.update((ams) =>
|
|
236
|
+
ams.map((am) =>
|
|
237
|
+
am.id === arrayMappingId
|
|
238
|
+
? { ...am, itemMappings: [...am.itemMappings, newMapping] }
|
|
239
|
+
: am
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// If this is part of an array-to-object mapping, add to itemMappings
|
|
245
|
+
if (arrayToObjectMappingId) {
|
|
246
|
+
this.arrayToObjectMappings.update((ams) =>
|
|
247
|
+
ams.map((am) =>
|
|
248
|
+
am.id === arrayToObjectMappingId
|
|
249
|
+
? { ...am, itemMappings: [...am.itemMappings, newMapping] }
|
|
250
|
+
: am
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return newMapping;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private createArrayMapping(
|
|
259
|
+
sourceArray: SchemaField,
|
|
260
|
+
targetArray: SchemaField
|
|
261
|
+
): FieldMapping {
|
|
262
|
+
// Check if array mapping already exists
|
|
263
|
+
const existingArrayMapping = this.arrayMappings().find(
|
|
264
|
+
(am) => am.sourceArray.id === sourceArray.id && am.targetArray.id === targetArray.id
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (existingArrayMapping) {
|
|
268
|
+
// Return a dummy field mapping representing the array mapping
|
|
269
|
+
const existing = this.mappings().find(m => m.id === existingArrayMapping.id);
|
|
270
|
+
if (existing) return existing;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const arrayMapping: ArrayMapping = {
|
|
274
|
+
id: this.generateId(),
|
|
275
|
+
sourceArray,
|
|
276
|
+
targetArray,
|
|
277
|
+
itemMappings: [],
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
this.arrayMappings.update((ams) => [...ams, arrayMapping]);
|
|
281
|
+
|
|
282
|
+
// Also create a field mapping to visualize the array connection
|
|
283
|
+
const fieldMapping: FieldMapping = {
|
|
284
|
+
id: arrayMapping.id,
|
|
285
|
+
sourceFields: [sourceArray],
|
|
286
|
+
targetField: targetArray,
|
|
287
|
+
transformations: [{ type: 'direct' }],
|
|
288
|
+
isArrayMapping: true,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
this.mappings.update((mappings) => [...mappings, fieldMapping]);
|
|
292
|
+
|
|
293
|
+
return fieldMapping;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private createArrayToObjectMapping(
|
|
297
|
+
sourceArray: SchemaField,
|
|
298
|
+
targetObject: SchemaField
|
|
299
|
+
): FieldMapping {
|
|
300
|
+
// Check if array-to-object mapping already exists
|
|
301
|
+
const existingMapping = this.arrayToObjectMappings().find(
|
|
302
|
+
(am) => am.sourceArray.id === sourceArray.id && am.targetObject.id === targetObject.id
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (existingMapping) {
|
|
306
|
+
const existing = this.mappings().find(m => m.id === existingMapping.id);
|
|
307
|
+
if (existing) return existing;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const arrayToObjectMapping: ArrayToObjectMapping = {
|
|
311
|
+
id: this.generateId(),
|
|
312
|
+
sourceArray,
|
|
313
|
+
targetObject,
|
|
314
|
+
selector: { mode: 'first' }, // Default to first item
|
|
315
|
+
itemMappings: [],
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
this.arrayToObjectMappings.update((ams) => [...ams, arrayToObjectMapping]);
|
|
319
|
+
|
|
320
|
+
// Create a field mapping to visualize the connection
|
|
321
|
+
const fieldMapping: FieldMapping = {
|
|
322
|
+
id: arrayToObjectMapping.id,
|
|
323
|
+
sourceFields: [sourceArray],
|
|
324
|
+
targetField: targetObject,
|
|
325
|
+
transformations: [{ type: 'direct' }],
|
|
326
|
+
isArrayToObjectMapping: true,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
this.mappings.update((mappings) => [...mappings, fieldMapping]);
|
|
330
|
+
|
|
331
|
+
return fieldMapping;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private findOrCreateArrayContext(
|
|
335
|
+
sourceField: SchemaField,
|
|
336
|
+
targetField: SchemaField
|
|
337
|
+
): string | undefined {
|
|
338
|
+
// If both fields are array items, check if an array mapping exists
|
|
339
|
+
if (sourceField.isArrayItem && targetField.isArrayItem) {
|
|
340
|
+
const existingArrayMapping = this.arrayMappings().find(
|
|
341
|
+
(am) =>
|
|
342
|
+
am.sourceArray.path === sourceField.parentArrayPath &&
|
|
343
|
+
am.targetArray.path === targetField.parentArrayPath
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (existingArrayMapping) {
|
|
347
|
+
return existingArrayMapping.id;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private findOrCreateArrayToObjectContext(
|
|
355
|
+
sourceField: SchemaField,
|
|
356
|
+
targetField: SchemaField
|
|
357
|
+
): string | undefined {
|
|
358
|
+
// If source is array item and target is within an object (not array item)
|
|
359
|
+
if (sourceField.isArrayItem && !targetField.isArrayItem && targetField.path.includes('.')) {
|
|
360
|
+
// Find the parent object path
|
|
361
|
+
const targetParts = targetField.path.split('.');
|
|
362
|
+
const parentObjectPath = targetParts.slice(0, -1).join('.');
|
|
363
|
+
|
|
364
|
+
const existingMapping = this.arrayToObjectMappings().find(
|
|
365
|
+
(am) =>
|
|
366
|
+
am.sourceArray.path === sourceField.parentArrayPath &&
|
|
367
|
+
am.targetObject.path === parentObjectPath
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (existingMapping) {
|
|
371
|
+
return existingMapping.id;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getArrayMapping(id: string): ArrayMapping | undefined {
|
|
379
|
+
return this.arrayMappings().find((am) => am.id === id);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
getArrayMappingForField(field: SchemaField): ArrayMapping | undefined {
|
|
383
|
+
if (!field.parentArrayPath) return undefined;
|
|
384
|
+
return this.arrayMappings().find(
|
|
385
|
+
(am) =>
|
|
386
|
+
am.sourceArray.path === field.parentArrayPath ||
|
|
387
|
+
am.targetArray.path === field.parentArrayPath
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
removeArrayMapping(arrayMappingId: string): void {
|
|
392
|
+
// Remove the array mapping
|
|
393
|
+
this.arrayMappings.update((ams) => ams.filter((am) => am.id !== arrayMappingId));
|
|
394
|
+
|
|
395
|
+
// Remove all field mappings associated with this array mapping
|
|
396
|
+
this.mappings.update((mappings) =>
|
|
397
|
+
mappings.filter((m) => m.arrayMappingId !== arrayMappingId && m.id !== arrayMappingId)
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
updateArrayFilter(arrayMappingId: string, filter: ArrayFilterConfig | undefined): void {
|
|
402
|
+
this.arrayMappings.update((ams) =>
|
|
403
|
+
ams.map((am) =>
|
|
404
|
+
am.id === arrayMappingId ? { ...am, filter } : am
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
getArrayToObjectMapping(id: string): ArrayToObjectMapping | undefined {
|
|
410
|
+
return this.arrayToObjectMappings().find((am) => am.id === id);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
updateArrayToObjectSelector(mappingId: string, selector: ArraySelectorConfig): void {
|
|
414
|
+
this.arrayToObjectMappings.update((ams) =>
|
|
415
|
+
ams.map((am) =>
|
|
416
|
+
am.id === mappingId ? { ...am, selector } : am
|
|
417
|
+
)
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
removeArrayToObjectMapping(mappingId: string): void {
|
|
422
|
+
// Remove the array-to-object mapping
|
|
423
|
+
this.arrayToObjectMappings.update((ams) => ams.filter((am) => am.id !== mappingId));
|
|
424
|
+
|
|
425
|
+
// Remove all field mappings associated with this mapping
|
|
426
|
+
this.mappings.update((mappings) =>
|
|
427
|
+
mappings.filter((m) => m.arrayToObjectMappingId !== mappingId && m.id !== mappingId)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
updateMapping(
|
|
432
|
+
mappingId: string,
|
|
433
|
+
updates: Partial<FieldMapping>
|
|
434
|
+
): void {
|
|
435
|
+
this.mappings.update((mappings) =>
|
|
436
|
+
mappings.map((m) => (m.id === mappingId ? { ...m, ...updates } : m))
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
updateTransformations(
|
|
441
|
+
mappingId: string,
|
|
442
|
+
transformations: TransformationConfig[]
|
|
443
|
+
): void {
|
|
444
|
+
this.mappings.update((mappings) =>
|
|
445
|
+
mappings.map((m) =>
|
|
446
|
+
m.id === mappingId ? { ...m, transformations } : m
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
removeMapping(mappingId: string): void {
|
|
452
|
+
this.mappings.update((mappings) =>
|
|
453
|
+
mappings.filter((m) => m.id !== mappingId)
|
|
454
|
+
);
|
|
455
|
+
if (this.selectedMappingId() === mappingId) {
|
|
456
|
+
this.selectedMappingId.set(null);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
removeSourceFromMapping(mappingId: string, sourceFieldId: string): void {
|
|
461
|
+
const mapping = this.mappings().find((m) => m.id === mappingId);
|
|
462
|
+
if (!mapping) return;
|
|
463
|
+
|
|
464
|
+
if (mapping.sourceFields.length <= 1) {
|
|
465
|
+
this.removeMapping(mappingId);
|
|
466
|
+
} else {
|
|
467
|
+
this.mappings.update((mappings) =>
|
|
468
|
+
mappings.map((m) =>
|
|
469
|
+
m.id === mappingId
|
|
470
|
+
? {
|
|
471
|
+
...m,
|
|
472
|
+
sourceFields: m.sourceFields.filter(
|
|
473
|
+
(sf) => sf.id !== sourceFieldId
|
|
474
|
+
),
|
|
475
|
+
transformations:
|
|
476
|
+
m.sourceFields.length - 1 === 1
|
|
477
|
+
? [{ type: 'direct' }]
|
|
478
|
+
: m.transformations,
|
|
479
|
+
}
|
|
480
|
+
: m
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
selectMapping(mappingId: string | null): void {
|
|
487
|
+
this.selectedMappingId.set(mappingId);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
getMappingForTarget(targetFieldId: string): FieldMapping | undefined {
|
|
491
|
+
return this.mappings().find((m) => m.targetField.id === targetFieldId);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
getMappingsForSource(sourceFieldId: string): FieldMapping[] {
|
|
495
|
+
return this.mappings().filter((m) =>
|
|
496
|
+
m.sourceFields.some((sf) => sf.id === sourceFieldId)
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
clearAllMappings(): void {
|
|
501
|
+
this.mappings.set([]);
|
|
502
|
+
this.arrayMappings.set([]);
|
|
503
|
+
this.arrayToObjectMappings.set([]);
|
|
504
|
+
this.defaultValues.set([]);
|
|
505
|
+
this.selectedMappingId.set(null);
|
|
506
|
+
this._sourceSchemaRef.set(null);
|
|
507
|
+
this._targetSchemaRef.set(null);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
setSourceSchemaRef(ref: string | null): void {
|
|
511
|
+
this._sourceSchemaRef.set(ref);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
setTargetSchemaRef(ref: string | null): void {
|
|
515
|
+
this._targetSchemaRef.set(ref);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Default value methods
|
|
519
|
+
setDefaultValue(targetField: SchemaField, value: string | number | boolean | Date | null): DefaultValue {
|
|
520
|
+
const existingDefault = this.defaultValues().find(d => d.targetField.id === targetField.id);
|
|
521
|
+
|
|
522
|
+
if (existingDefault) {
|
|
523
|
+
const updated: DefaultValue = { ...existingDefault, value };
|
|
524
|
+
this.defaultValues.update(dv => dv.map(d => d.id === existingDefault.id ? updated : d));
|
|
525
|
+
return updated;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const newDefault: DefaultValue = {
|
|
529
|
+
id: this.generateId(),
|
|
530
|
+
targetField,
|
|
531
|
+
value,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
this.defaultValues.update(dv => [...dv, newDefault]);
|
|
535
|
+
return newDefault;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
getDefaultValue(targetFieldId: string): DefaultValue | undefined {
|
|
539
|
+
return this.defaultValues().find(d => d.targetField.id === targetFieldId);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
removeDefaultValue(targetFieldId: string): void {
|
|
543
|
+
this.defaultValues.update(dv => dv.filter(d => d.targetField.id !== targetFieldId));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
hasDefaultValue(targetFieldId: string): boolean {
|
|
547
|
+
return this.defaultValues().some(d => d.targetField.id === targetFieldId);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
exportMappings(name?: string, description?: string): string {
|
|
551
|
+
const exportData: Record<string, unknown> = {
|
|
552
|
+
version: '1.0',
|
|
553
|
+
name: name || 'Mapping Configuration',
|
|
554
|
+
description: description || '',
|
|
555
|
+
mappings: this.mappings(),
|
|
556
|
+
arrayMappings: this.arrayMappings(),
|
|
557
|
+
arrayToObjectMappings: this.arrayToObjectMappings(),
|
|
558
|
+
defaultValues: this.defaultValues(),
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// Include schema refs if set
|
|
562
|
+
if (this._sourceSchemaRef()) {
|
|
563
|
+
exportData['sourceSchemaRef'] = this._sourceSchemaRef();
|
|
564
|
+
}
|
|
565
|
+
if (this._targetSchemaRef()) {
|
|
566
|
+
exportData['targetSchemaRef'] = this._targetSchemaRef();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return JSON.stringify(exportData, null, 2);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Export mappings as a MappingDocument object (not stringified)
|
|
574
|
+
*/
|
|
575
|
+
exportMappingsAsObject(name?: string, description?: string): object {
|
|
576
|
+
const exportData: Record<string, unknown> = {
|
|
577
|
+
version: '1.0',
|
|
578
|
+
name: name || 'Mapping Configuration',
|
|
579
|
+
description: description || '',
|
|
580
|
+
mappings: this.mappings(),
|
|
581
|
+
arrayMappings: this.arrayMappings(),
|
|
582
|
+
arrayToObjectMappings: this.arrayToObjectMappings(),
|
|
583
|
+
defaultValues: this.defaultValues(),
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Include schema refs if set
|
|
587
|
+
if (this._sourceSchemaRef()) {
|
|
588
|
+
exportData['sourceSchemaRef'] = this._sourceSchemaRef();
|
|
589
|
+
}
|
|
590
|
+
if (this._targetSchemaRef()) {
|
|
591
|
+
exportData['targetSchemaRef'] = this._targetSchemaRef();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return exportData;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
importMappings(json: string): void {
|
|
598
|
+
try {
|
|
599
|
+
const data = JSON.parse(json);
|
|
600
|
+
if (data.mappings) {
|
|
601
|
+
this.mappings.set(data.mappings as FieldMapping[]);
|
|
602
|
+
this.arrayMappings.set(data.arrayMappings as ArrayMapping[] || []);
|
|
603
|
+
this.arrayToObjectMappings.set(data.arrayToObjectMappings as ArrayToObjectMapping[] || []);
|
|
604
|
+
this.defaultValues.set(data.defaultValues as DefaultValue[] || []);
|
|
605
|
+
this._sourceSchemaRef.set(data.sourceSchemaRef || null);
|
|
606
|
+
this._targetSchemaRef.set(data.targetSchemaRef || null);
|
|
607
|
+
} else {
|
|
608
|
+
// Legacy format
|
|
609
|
+
this.mappings.set(data as FieldMapping[]);
|
|
610
|
+
}
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error('Failed to import mappings:', e);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|