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.
Files changed (64) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/LICENSE +190 -0
  3. package/PUBLISHING.md +75 -0
  4. package/README.md +214 -0
  5. package/angular.json +121 -0
  6. package/package.json +67 -0
  7. package/projects/demo-app/public/favicon.ico +0 -0
  8. package/projects/demo-app/src/app/app.config.ts +12 -0
  9. package/projects/demo-app/src/app/app.html +36 -0
  10. package/projects/demo-app/src/app/app.routes.ts +62 -0
  11. package/projects/demo-app/src/app/app.scss +65 -0
  12. package/projects/demo-app/src/app/app.ts +11 -0
  13. package/projects/demo-app/src/app/layout/app-layout.component.ts +294 -0
  14. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.html +87 -0
  15. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.scss +202 -0
  16. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.ts +192 -0
  17. package/projects/demo-app/src/app/pages/mappings-page/add-mapping-dialog.component.ts +163 -0
  18. package/projects/demo-app/src/app/pages/mappings-page/mappings-page.component.ts +306 -0
  19. package/projects/demo-app/src/app/pages/schema-creator-page/schema-creator-page.component.ts +88 -0
  20. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.html +108 -0
  21. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.scss +317 -0
  22. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.ts +129 -0
  23. package/projects/demo-app/src/app/services/app-state.service.ts +233 -0
  24. package/projects/demo-app/src/app/services/sample-data.service.ts +228 -0
  25. package/projects/demo-app/src/index.html +15 -0
  26. package/projects/demo-app/src/main.ts +6 -0
  27. package/projects/demo-app/src/styles.scss +54 -0
  28. package/projects/demo-app/tsconfig.app.json +13 -0
  29. package/projects/ngx-data-mapper/ng-package.json +7 -0
  30. package/projects/ngx-data-mapper/package.json +40 -0
  31. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.html +183 -0
  32. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.scss +352 -0
  33. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.ts +277 -0
  34. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.html +174 -0
  35. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.scss +357 -0
  36. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.ts +258 -0
  37. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.html +139 -0
  38. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.scss +213 -0
  39. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.ts +261 -0
  40. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.html +199 -0
  41. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.scss +321 -0
  42. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.ts +618 -0
  43. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.html +67 -0
  44. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.scss +97 -0
  45. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.ts +105 -0
  46. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.html +552 -0
  47. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.scss +824 -0
  48. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.ts +730 -0
  49. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.html +82 -0
  50. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.scss +352 -0
  51. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.ts +225 -0
  52. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.html +346 -0
  53. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.scss +511 -0
  54. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.ts +368 -0
  55. package/projects/ngx-data-mapper/src/lib/models/json-schema.model.ts +164 -0
  56. package/projects/ngx-data-mapper/src/lib/models/schema.model.ts +173 -0
  57. package/projects/ngx-data-mapper/src/lib/services/mapping.service.ts +615 -0
  58. package/projects/ngx-data-mapper/src/lib/services/schema-parser.service.ts +270 -0
  59. package/projects/ngx-data-mapper/src/lib/services/svg-connector.service.ts +135 -0
  60. package/projects/ngx-data-mapper/src/lib/services/transformation.service.ts +453 -0
  61. package/projects/ngx-data-mapper/src/public-api.ts +22 -0
  62. package/projects/ngx-data-mapper/tsconfig.lib.json +13 -0
  63. package/projects/ngx-data-mapper/tsconfig.lib.prod.json +9 -0
  64. 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
+ }