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,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Input,
|
|
4
|
+
Output,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
OnInit,
|
|
7
|
+
OnChanges,
|
|
8
|
+
SimpleChanges,
|
|
9
|
+
inject,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import { CommonModule } from '@angular/common';
|
|
12
|
+
import { FormsModule } from '@angular/forms';
|
|
13
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
14
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
15
|
+
import { MatSelectModule } from '@angular/material/select';
|
|
16
|
+
import { MatInputModule } from '@angular/material/input';
|
|
17
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
18
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
19
|
+
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
20
|
+
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
|
21
|
+
import {
|
|
22
|
+
FieldMapping,
|
|
23
|
+
FilterGroup,
|
|
24
|
+
TransformationConfig,
|
|
25
|
+
TransformationType,
|
|
26
|
+
} from '../../models/schema.model';
|
|
27
|
+
import { ConditionBuilderComponent } from '../condition-builder/condition-builder.component';
|
|
28
|
+
import { TransformationService } from '../../services/transformation.service';
|
|
29
|
+
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'transformation-popover',
|
|
32
|
+
standalone: true,
|
|
33
|
+
imports: [
|
|
34
|
+
CommonModule,
|
|
35
|
+
FormsModule,
|
|
36
|
+
MatIconModule,
|
|
37
|
+
MatButtonModule,
|
|
38
|
+
MatSelectModule,
|
|
39
|
+
MatInputModule,
|
|
40
|
+
MatFormFieldModule,
|
|
41
|
+
MatTooltipModule,
|
|
42
|
+
MatCheckboxModule,
|
|
43
|
+
DragDropModule,
|
|
44
|
+
ConditionBuilderComponent,
|
|
45
|
+
],
|
|
46
|
+
templateUrl: './transformation-popover.component.html',
|
|
47
|
+
styleUrl: './transformation-popover.component.scss',
|
|
48
|
+
})
|
|
49
|
+
export class TransformationPopoverComponent implements OnInit, OnChanges {
|
|
50
|
+
@Input() mapping!: FieldMapping;
|
|
51
|
+
@Input() position: { x: number; y: number } = { x: 0, y: 0 };
|
|
52
|
+
@Input() sampleData: Record<string, unknown> = {};
|
|
53
|
+
|
|
54
|
+
@Output() save = new EventEmitter<TransformationConfig[]>();
|
|
55
|
+
@Output() delete = new EventEmitter<void>();
|
|
56
|
+
@Output() close = new EventEmitter<void>();
|
|
57
|
+
|
|
58
|
+
private transformationService = inject(TransformationService);
|
|
59
|
+
|
|
60
|
+
// Array of transformation steps
|
|
61
|
+
steps: TransformationConfig[] = [];
|
|
62
|
+
|
|
63
|
+
// Preview results for each step
|
|
64
|
+
stepPreviews: string[] = [];
|
|
65
|
+
// Input values for each step (to show input → output)
|
|
66
|
+
stepInputs: string[] = [];
|
|
67
|
+
finalPreview: string = '';
|
|
68
|
+
|
|
69
|
+
// Track which step is expanded (-1 means none, used in single-step mode)
|
|
70
|
+
expandedStepIndex: number = -1;
|
|
71
|
+
|
|
72
|
+
availableTransformations = this.transformationService.getAvailableTransformations();
|
|
73
|
+
|
|
74
|
+
ngOnInit(): void {
|
|
75
|
+
this.initFromMapping();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
79
|
+
if (changes['mapping'] || changes['sampleData']) {
|
|
80
|
+
this.initFromMapping();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private initFromMapping(): void {
|
|
85
|
+
if (this.mapping) {
|
|
86
|
+
// Copy transformations array, with fallback for empty/undefined
|
|
87
|
+
if (this.mapping.transformations && this.mapping.transformations.length > 0) {
|
|
88
|
+
this.steps = this.mapping.transformations.map(t => ({ ...t }));
|
|
89
|
+
} else {
|
|
90
|
+
// Fallback to a single direct transformation
|
|
91
|
+
this.steps = [{ type: 'direct' }];
|
|
92
|
+
}
|
|
93
|
+
this.updatePreview();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if we're in multi-step mode (more than one step)
|
|
98
|
+
get isMultiStep(): boolean {
|
|
99
|
+
return this.steps.length > 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onStepTypeChange(index: number): void {
|
|
103
|
+
const step = this.steps[index];
|
|
104
|
+
|
|
105
|
+
// Set defaults based on type
|
|
106
|
+
switch (step.type) {
|
|
107
|
+
case 'concat':
|
|
108
|
+
step.separator = step.separator ?? ' ';
|
|
109
|
+
step.template = step.template ?? this.getDefaultTemplate();
|
|
110
|
+
break;
|
|
111
|
+
case 'substring':
|
|
112
|
+
step.startIndex = step.startIndex ?? 0;
|
|
113
|
+
step.endIndex = step.endIndex ?? 10;
|
|
114
|
+
break;
|
|
115
|
+
case 'replace':
|
|
116
|
+
step.searchValue = step.searchValue ?? '';
|
|
117
|
+
step.replaceValue = step.replaceValue ?? '';
|
|
118
|
+
break;
|
|
119
|
+
case 'dateFormat':
|
|
120
|
+
step.outputFormat = step.outputFormat ?? 'YYYY-MM-DD';
|
|
121
|
+
break;
|
|
122
|
+
case 'numberFormat':
|
|
123
|
+
step.decimalPlaces = step.decimalPlaces ?? 2;
|
|
124
|
+
break;
|
|
125
|
+
case 'mask':
|
|
126
|
+
step.pattern = step.pattern ?? '(###) ###-####';
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.updatePreview();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private getDefaultTemplate(): string {
|
|
134
|
+
return this.mapping.sourceFields.map((_, i) => `{${i}}`).join(' ');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
addStep(): void {
|
|
138
|
+
this.steps.push({ type: 'direct' });
|
|
139
|
+
// Expand the newly added step
|
|
140
|
+
this.expandedStepIndex = this.steps.length - 1;
|
|
141
|
+
this.updatePreview();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
removeStep(index: number): void {
|
|
145
|
+
if (this.steps.length > 1) {
|
|
146
|
+
this.steps.splice(index, 1);
|
|
147
|
+
// Collapse after deletion
|
|
148
|
+
this.expandedStepIndex = -1;
|
|
149
|
+
this.updatePreview();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
onStepDrop(event: CdkDragDrop<TransformationConfig[]>): void {
|
|
154
|
+
if (event.previousIndex !== event.currentIndex) {
|
|
155
|
+
moveItemInArray(this.steps, event.previousIndex, event.currentIndex);
|
|
156
|
+
// Move expanded index with the step
|
|
157
|
+
if (this.expandedStepIndex === event.previousIndex) {
|
|
158
|
+
this.expandedStepIndex = event.currentIndex;
|
|
159
|
+
} else if (
|
|
160
|
+
this.expandedStepIndex > event.previousIndex &&
|
|
161
|
+
this.expandedStepIndex <= event.currentIndex
|
|
162
|
+
) {
|
|
163
|
+
this.expandedStepIndex--;
|
|
164
|
+
} else if (
|
|
165
|
+
this.expandedStepIndex < event.previousIndex &&
|
|
166
|
+
this.expandedStepIndex >= event.currentIndex
|
|
167
|
+
) {
|
|
168
|
+
this.expandedStepIndex++;
|
|
169
|
+
}
|
|
170
|
+
this.updatePreview();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toggleStep(index: number): void {
|
|
175
|
+
if (this.expandedStepIndex === index) {
|
|
176
|
+
this.expandedStepIndex = -1; // Collapse
|
|
177
|
+
} else {
|
|
178
|
+
this.expandedStepIndex = index; // Expand
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
isStepExpanded(index: number): boolean {
|
|
183
|
+
return this.expandedStepIndex === index;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
updatePreview(): void {
|
|
187
|
+
if (!this.mapping || !this.sampleData) {
|
|
188
|
+
this.stepPreviews = [];
|
|
189
|
+
this.stepInputs = [];
|
|
190
|
+
this.finalPreview = '';
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.stepPreviews = [];
|
|
195
|
+
this.stepInputs = [];
|
|
196
|
+
let currentValue: unknown = null;
|
|
197
|
+
|
|
198
|
+
// Get initial input from source fields
|
|
199
|
+
const initialValues = this.mapping.sourceFields
|
|
200
|
+
.map(f => this.getValueByPath(this.sampleData, f.path))
|
|
201
|
+
.map(v => String(v ?? ''));
|
|
202
|
+
const initialInput = initialValues.join(', ');
|
|
203
|
+
|
|
204
|
+
// For the first step, use the source fields from sample data
|
|
205
|
+
for (let i = 0; i < this.steps.length; i++) {
|
|
206
|
+
const step = this.steps[i];
|
|
207
|
+
|
|
208
|
+
if (i === 0) {
|
|
209
|
+
// First step: input is from source fields
|
|
210
|
+
this.stepInputs.push(initialInput);
|
|
211
|
+
// Check condition before applying transformation
|
|
212
|
+
if (this.transformationService.isConditionMet(initialInput, step)) {
|
|
213
|
+
currentValue = this.transformationService.applyTransformation(
|
|
214
|
+
this.sampleData,
|
|
215
|
+
this.mapping.sourceFields,
|
|
216
|
+
step
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
currentValue = initialInput; // Pass through if condition not met
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// Subsequent steps: input is previous output
|
|
223
|
+
this.stepInputs.push(this.stepPreviews[i - 1]);
|
|
224
|
+
// Check condition before applying transformation
|
|
225
|
+
if (this.transformationService.isConditionMet(currentValue, step)) {
|
|
226
|
+
currentValue = this.transformationService.applyTransformationToValue(
|
|
227
|
+
currentValue,
|
|
228
|
+
step
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
// If condition not met, currentValue passes through unchanged
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.stepPreviews.push(String(currentValue ?? ''));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.finalPreview = this.stepPreviews[this.stepPreviews.length - 1] || '';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private getValueByPath(obj: Record<string, unknown>, path: string): unknown {
|
|
241
|
+
return path.split('.').reduce((acc: unknown, part) => {
|
|
242
|
+
if (acc && typeof acc === 'object') {
|
|
243
|
+
return (acc as Record<string, unknown>)[part];
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}, obj);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
onConfigChange(): void {
|
|
250
|
+
this.updatePreview();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
onSave(): void {
|
|
254
|
+
// Filter out 'direct' transformations if they're not the only one
|
|
255
|
+
const cleanedSteps = this.steps.length === 1
|
|
256
|
+
? this.steps
|
|
257
|
+
: this.steps.filter(s => s.type !== 'direct');
|
|
258
|
+
|
|
259
|
+
// If all filtered out, keep at least one direct
|
|
260
|
+
const finalSteps = cleanedSteps.length > 0 ? cleanedSteps : [{ type: 'direct' as TransformationType }];
|
|
261
|
+
|
|
262
|
+
this.save.emit(finalSteps);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
onDelete(): void {
|
|
266
|
+
this.delete.emit();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
onClose(): void {
|
|
270
|
+
this.close.emit();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
getSourceFieldNames(): string {
|
|
274
|
+
return this.mapping.sourceFields.map((f) => f.name).join(', ');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getPopoverStyle(): Record<string, string> {
|
|
278
|
+
return {
|
|
279
|
+
left: `${this.position.x}px`,
|
|
280
|
+
top: `${this.position.y}px`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getStepTypeLabel(type: TransformationType): string {
|
|
285
|
+
const t = this.availableTransformations.find(t => t.type === type);
|
|
286
|
+
return t?.label || type;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Condition methods
|
|
290
|
+
hasCondition(step: TransformationConfig): boolean {
|
|
291
|
+
return step.condition?.enabled === true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
toggleCondition(step: TransformationConfig, enabled: boolean): void {
|
|
295
|
+
if (enabled) {
|
|
296
|
+
if (!step.condition) {
|
|
297
|
+
step.condition = {
|
|
298
|
+
enabled: true,
|
|
299
|
+
root: this.createEmptyConditionGroup(),
|
|
300
|
+
};
|
|
301
|
+
} else {
|
|
302
|
+
step.condition.enabled = true;
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
if (step.condition) {
|
|
306
|
+
step.condition.enabled = false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
onConditionChange(step: TransformationConfig, group: FilterGroup): void {
|
|
312
|
+
if (step.condition) {
|
|
313
|
+
step.condition.root = group;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private createEmptyConditionGroup(): FilterGroup {
|
|
318
|
+
return {
|
|
319
|
+
id: `cond-${Date.now()}`,
|
|
320
|
+
type: 'group',
|
|
321
|
+
logic: 'and',
|
|
322
|
+
children: [],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
getConditionSummary(step: TransformationConfig): string {
|
|
327
|
+
if (!step.condition?.enabled || !step.condition.root) return '';
|
|
328
|
+
return this.summarizeConditionGroup(step.condition.root);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private summarizeConditionGroup(group: FilterGroup): string {
|
|
332
|
+
if (group.children.length === 0) return '';
|
|
333
|
+
|
|
334
|
+
const parts = group.children.map(child => {
|
|
335
|
+
if (child.type === 'condition') {
|
|
336
|
+
const opLabel = this.getOperatorLabel(child.operator);
|
|
337
|
+
if (['isEmpty', 'isNotEmpty', 'isTrue', 'isFalse'].includes(child.operator)) {
|
|
338
|
+
return opLabel;
|
|
339
|
+
}
|
|
340
|
+
return `${opLabel} "${child.value}"`;
|
|
341
|
+
} else {
|
|
342
|
+
return `(${this.summarizeConditionGroup(child)})`;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return parts.join(` ${group.logic.toUpperCase()} `);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private getOperatorLabel(operator: string): string {
|
|
350
|
+
const labels: Record<string, string> = {
|
|
351
|
+
equals: '=',
|
|
352
|
+
notEquals: '!=',
|
|
353
|
+
contains: 'contains',
|
|
354
|
+
notContains: 'not contain',
|
|
355
|
+
startsWith: 'starts with',
|
|
356
|
+
endsWith: 'ends with',
|
|
357
|
+
isEmpty: 'is empty',
|
|
358
|
+
isNotEmpty: 'is not empty',
|
|
359
|
+
greaterThan: '>',
|
|
360
|
+
lessThan: '<',
|
|
361
|
+
greaterThanOrEqual: '>=',
|
|
362
|
+
lessThanOrEqual: '<=',
|
|
363
|
+
isTrue: 'is true',
|
|
364
|
+
isFalse: 'is false',
|
|
365
|
+
};
|
|
366
|
+
return labels[operator] || operator;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard JSON Schema (draft-07) TypeScript interfaces
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface JsonSchema {
|
|
6
|
+
$schema?: string;
|
|
7
|
+
$id?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
type?: JsonSchemaType | JsonSchemaType[];
|
|
11
|
+
properties?: Record<string, JsonSchema>;
|
|
12
|
+
items?: JsonSchema;
|
|
13
|
+
required?: string[];
|
|
14
|
+
enum?: (string | number | boolean | null)[];
|
|
15
|
+
const?: unknown;
|
|
16
|
+
default?: unknown;
|
|
17
|
+
|
|
18
|
+
// String validations
|
|
19
|
+
minLength?: number;
|
|
20
|
+
maxLength?: number;
|
|
21
|
+
pattern?: string;
|
|
22
|
+
format?: string;
|
|
23
|
+
|
|
24
|
+
// Number validations
|
|
25
|
+
minimum?: number;
|
|
26
|
+
maximum?: number;
|
|
27
|
+
exclusiveMinimum?: number;
|
|
28
|
+
exclusiveMaximum?: number;
|
|
29
|
+
multipleOf?: number;
|
|
30
|
+
|
|
31
|
+
// Array validations
|
|
32
|
+
minItems?: number;
|
|
33
|
+
maxItems?: number;
|
|
34
|
+
uniqueItems?: boolean;
|
|
35
|
+
|
|
36
|
+
// Object validations
|
|
37
|
+
minProperties?: number;
|
|
38
|
+
maxProperties?: number;
|
|
39
|
+
additionalProperties?: boolean | JsonSchema;
|
|
40
|
+
|
|
41
|
+
// Combining schemas
|
|
42
|
+
allOf?: JsonSchema[];
|
|
43
|
+
anyOf?: JsonSchema[];
|
|
44
|
+
oneOf?: JsonSchema[];
|
|
45
|
+
not?: JsonSchema;
|
|
46
|
+
|
|
47
|
+
// References
|
|
48
|
+
$ref?: string;
|
|
49
|
+
definitions?: Record<string, JsonSchema>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type JsonSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Helper type for working with JSON schemas in the UI
|
|
56
|
+
*/
|
|
57
|
+
export interface JsonSchemaField {
|
|
58
|
+
name: string;
|
|
59
|
+
path: string;
|
|
60
|
+
schema: JsonSchema;
|
|
61
|
+
children?: JsonSchemaField[];
|
|
62
|
+
expanded?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert JSON Schema to flat field list for UI rendering
|
|
67
|
+
*/
|
|
68
|
+
export function schemaToFields(schema: JsonSchema, parentPath: string = ''): JsonSchemaField[] {
|
|
69
|
+
const fields: JsonSchemaField[] = [];
|
|
70
|
+
|
|
71
|
+
if (schema.type === 'object' && schema.properties) {
|
|
72
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
73
|
+
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
74
|
+
const field: JsonSchemaField = {
|
|
75
|
+
name,
|
|
76
|
+
path,
|
|
77
|
+
schema: propSchema,
|
|
78
|
+
expanded: false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (propSchema.type === 'object' && propSchema.properties) {
|
|
82
|
+
field.children = schemaToFields(propSchema, path);
|
|
83
|
+
} else if (propSchema.type === 'array' && propSchema.items) {
|
|
84
|
+
field.children = schemaToFields(propSchema.items, `${path}[]`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fields.push(field);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return fields;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the simple type for display purposes
|
|
96
|
+
*/
|
|
97
|
+
export function getSchemaType(schema: JsonSchema): string {
|
|
98
|
+
if (Array.isArray(schema.type)) {
|
|
99
|
+
return schema.type.filter(t => t !== 'null').join(' | ');
|
|
100
|
+
}
|
|
101
|
+
if (schema.type === 'integer') {
|
|
102
|
+
return 'number';
|
|
103
|
+
}
|
|
104
|
+
if (schema.format === 'date' || schema.format === 'date-time') {
|
|
105
|
+
return 'date';
|
|
106
|
+
}
|
|
107
|
+
return schema.type || 'any';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create an empty JSON Schema for a new schema definition
|
|
112
|
+
*/
|
|
113
|
+
export function createEmptySchema(title: string = 'New Schema'): JsonSchema {
|
|
114
|
+
return {
|
|
115
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
116
|
+
title,
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {},
|
|
119
|
+
required: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Add a property to a schema
|
|
125
|
+
*/
|
|
126
|
+
export function addProperty(
|
|
127
|
+
schema: JsonSchema,
|
|
128
|
+
name: string,
|
|
129
|
+
type: JsonSchemaType,
|
|
130
|
+
options?: { description?: string; required?: boolean }
|
|
131
|
+
): JsonSchema {
|
|
132
|
+
const newSchema = { ...schema };
|
|
133
|
+
newSchema.properties = { ...newSchema.properties };
|
|
134
|
+
|
|
135
|
+
const propSchema: JsonSchema = { type };
|
|
136
|
+
if (options?.description) {
|
|
137
|
+
propSchema.description = options.description;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (type === 'object') {
|
|
141
|
+
propSchema.properties = {};
|
|
142
|
+
} else if (type === 'array') {
|
|
143
|
+
propSchema.items = { type: 'string' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
newSchema.properties[name] = propSchema;
|
|
147
|
+
|
|
148
|
+
if (options?.required) {
|
|
149
|
+
newSchema.required = [...(newSchema.required || []), name];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return newSchema;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Remove a property from a schema
|
|
157
|
+
*/
|
|
158
|
+
export function removeProperty(schema: JsonSchema, name: string): JsonSchema {
|
|
159
|
+
const newSchema = { ...schema };
|
|
160
|
+
newSchema.properties = { ...newSchema.properties };
|
|
161
|
+
delete newSchema.properties[name];
|
|
162
|
+
newSchema.required = (newSchema.required || []).filter(r => r !== name);
|
|
163
|
+
return newSchema;
|
|
164
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export interface SchemaField {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'date';
|
|
5
|
+
path: string;
|
|
6
|
+
children?: SchemaField[];
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
description?: string;
|
|
9
|
+
isArrayItem?: boolean; // Marks fields that are children of an array
|
|
10
|
+
parentArrayPath?: string; // Path to parent array for context
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SchemaDefinition {
|
|
14
|
+
name: string;
|
|
15
|
+
fields: SchemaField[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TransformationType =
|
|
19
|
+
| 'direct'
|
|
20
|
+
| 'concat'
|
|
21
|
+
| 'substring'
|
|
22
|
+
| 'replace'
|
|
23
|
+
| 'uppercase'
|
|
24
|
+
| 'lowercase'
|
|
25
|
+
| 'trim'
|
|
26
|
+
| 'mask'
|
|
27
|
+
| 'dateFormat'
|
|
28
|
+
| 'extractYear'
|
|
29
|
+
| 'extractMonth'
|
|
30
|
+
| 'extractDay'
|
|
31
|
+
| 'extractHour'
|
|
32
|
+
| 'extractMinute'
|
|
33
|
+
| 'extractSecond'
|
|
34
|
+
| 'numberFormat'
|
|
35
|
+
| 'template';
|
|
36
|
+
|
|
37
|
+
export interface TransformationCondition {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
root: FilterGroup;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TransformationConfig {
|
|
43
|
+
type: TransformationType;
|
|
44
|
+
// For concat
|
|
45
|
+
separator?: string;
|
|
46
|
+
template?: string;
|
|
47
|
+
// For substring
|
|
48
|
+
startIndex?: number;
|
|
49
|
+
endIndex?: number;
|
|
50
|
+
// For replace
|
|
51
|
+
searchValue?: string;
|
|
52
|
+
replaceValue?: string;
|
|
53
|
+
// For date format
|
|
54
|
+
inputFormat?: string;
|
|
55
|
+
outputFormat?: string;
|
|
56
|
+
// For number format
|
|
57
|
+
decimalPlaces?: number;
|
|
58
|
+
prefix?: string;
|
|
59
|
+
suffix?: string;
|
|
60
|
+
// For mask
|
|
61
|
+
pattern?: string;
|
|
62
|
+
// Optional condition - transformation only applies if condition is met
|
|
63
|
+
condition?: TransformationCondition;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface FieldMapping {
|
|
67
|
+
id: string;
|
|
68
|
+
sourceFields: SchemaField[];
|
|
69
|
+
targetField: SchemaField;
|
|
70
|
+
transformations: TransformationConfig[]; // Array of transformation steps applied in sequence
|
|
71
|
+
// For array-to-array mappings
|
|
72
|
+
isArrayMapping?: boolean;
|
|
73
|
+
arrayMappingId?: string; // Reference to parent array mapping
|
|
74
|
+
// For array-to-object mappings
|
|
75
|
+
isArrayToObjectMapping?: boolean;
|
|
76
|
+
arrayToObjectMappingId?: string; // Reference to parent array-to-object mapping
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type FilterOperator =
|
|
80
|
+
| 'equals'
|
|
81
|
+
| 'notEquals'
|
|
82
|
+
| 'contains'
|
|
83
|
+
| 'notContains'
|
|
84
|
+
| 'startsWith'
|
|
85
|
+
| 'endsWith'
|
|
86
|
+
| 'greaterThan'
|
|
87
|
+
| 'lessThan'
|
|
88
|
+
| 'greaterThanOrEqual'
|
|
89
|
+
| 'lessThanOrEqual'
|
|
90
|
+
| 'isEmpty'
|
|
91
|
+
| 'isNotEmpty'
|
|
92
|
+
| 'isTrue'
|
|
93
|
+
| 'isFalse';
|
|
94
|
+
|
|
95
|
+
export interface FilterCondition {
|
|
96
|
+
id: string;
|
|
97
|
+
type: 'condition';
|
|
98
|
+
field: string; // Field path within the array item
|
|
99
|
+
fieldName: string; // Display name
|
|
100
|
+
operator: FilterOperator;
|
|
101
|
+
value: string | number | boolean;
|
|
102
|
+
valueType: 'string' | 'number' | 'boolean';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FilterGroup {
|
|
106
|
+
id: string;
|
|
107
|
+
type: 'group';
|
|
108
|
+
logic: 'and' | 'or';
|
|
109
|
+
children: FilterItem[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type FilterItem = FilterCondition | FilterGroup;
|
|
113
|
+
|
|
114
|
+
export interface ArrayFilterConfig {
|
|
115
|
+
enabled: boolean;
|
|
116
|
+
root: FilterGroup;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ArrayMapping {
|
|
120
|
+
id: string;
|
|
121
|
+
sourceArray: SchemaField;
|
|
122
|
+
targetArray: SchemaField;
|
|
123
|
+
itemMappings: FieldMapping[]; // Mappings for fields within the array items
|
|
124
|
+
filter?: ArrayFilterConfig; // Optional filter on source array
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Selection mode for array-to-object mapping
|
|
128
|
+
export type ArraySelectionMode = 'first' | 'last' | 'condition';
|
|
129
|
+
|
|
130
|
+
export interface ArraySelectorConfig {
|
|
131
|
+
mode: ArraySelectionMode;
|
|
132
|
+
condition?: FilterGroup; // Reuse filter group for condition mode
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ArrayToObjectMapping {
|
|
136
|
+
id: string;
|
|
137
|
+
sourceArray: SchemaField;
|
|
138
|
+
targetObject: SchemaField;
|
|
139
|
+
selector: ArraySelectorConfig; // How to select the single item
|
|
140
|
+
itemMappings: FieldMapping[]; // Mappings for fields within the selected item
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ConnectionPoint {
|
|
144
|
+
fieldId: string;
|
|
145
|
+
side: 'source' | 'target';
|
|
146
|
+
x: number;
|
|
147
|
+
y: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface Connection {
|
|
151
|
+
id: string;
|
|
152
|
+
mappingId: string;
|
|
153
|
+
sourcePoints: ConnectionPoint[];
|
|
154
|
+
targetPoint: ConnectionPoint;
|
|
155
|
+
transformations: TransformationConfig[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface DragState {
|
|
159
|
+
isDragging: boolean;
|
|
160
|
+
sourceField: SchemaField | null;
|
|
161
|
+
startPoint: { x: number; y: number } | null;
|
|
162
|
+
currentPoint: { x: number; y: number } | null;
|
|
163
|
+
// For endpoint dragging (moving existing connection)
|
|
164
|
+
dragMode: 'new' | 'move-source' | 'move-target';
|
|
165
|
+
mappingId?: string; // The mapping being modified when moving endpoint
|
|
166
|
+
sourceFieldIndex?: number; // For multi-source mappings, which source is being moved
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface DefaultValue {
|
|
170
|
+
id: string;
|
|
171
|
+
targetField: SchemaField;
|
|
172
|
+
value: string | number | boolean | Date | null;
|
|
173
|
+
}
|