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,453 @@
1
+ import { Injectable } from '@angular/core';
2
+ import {
3
+ TransformationConfig,
4
+ TransformationType,
5
+ SchemaField,
6
+ FilterGroup,
7
+ FilterCondition,
8
+ FilterItem,
9
+ FilterOperator,
10
+ } from '../models/schema.model';
11
+
12
+ @Injectable({
13
+ providedIn: 'root',
14
+ })
15
+ export class TransformationService {
16
+ applyTransformation(
17
+ sourceValues: Record<string, unknown>,
18
+ sourceFields: SchemaField[],
19
+ config: TransformationConfig
20
+ ): string {
21
+ const values = sourceFields.map((f) => this.getValueByPath(sourceValues, f.path));
22
+
23
+ switch (config.type) {
24
+ case 'direct':
25
+ return String(values[0] ?? '');
26
+
27
+ case 'concat':
28
+ if (config.template) {
29
+ return this.applyTemplate(config.template, sourceFields, sourceValues);
30
+ }
31
+ return values.join(config.separator ?? ' ');
32
+
33
+ case 'substring':
34
+ const str = String(values[0] ?? '');
35
+ return str.substring(
36
+ config.startIndex ?? 0,
37
+ config.endIndex ?? str.length
38
+ );
39
+
40
+ case 'replace':
41
+ return String(values[0] ?? '').replace(
42
+ new RegExp(config.searchValue ?? '', 'g'),
43
+ config.replaceValue ?? ''
44
+ );
45
+
46
+ case 'uppercase':
47
+ return String(values[0] ?? '').toUpperCase();
48
+
49
+ case 'lowercase':
50
+ return String(values[0] ?? '').toLowerCase();
51
+
52
+ case 'trim':
53
+ return String(values[0] ?? '').trim();
54
+
55
+ case 'mask':
56
+ return this.applyMask(String(values[0] ?? ''), config.pattern ?? '');
57
+
58
+ case 'dateFormat':
59
+ return this.formatDate(
60
+ values[0],
61
+ config.inputFormat,
62
+ config.outputFormat
63
+ );
64
+
65
+ case 'extractYear':
66
+ return this.extractDatePart(values[0], 'year');
67
+
68
+ case 'extractMonth':
69
+ return this.extractDatePart(values[0], 'month');
70
+
71
+ case 'extractDay':
72
+ return this.extractDatePart(values[0], 'day');
73
+
74
+ case 'extractHour':
75
+ return this.extractDatePart(values[0], 'hour');
76
+
77
+ case 'extractMinute':
78
+ return this.extractDatePart(values[0], 'minute');
79
+
80
+ case 'extractSecond':
81
+ return this.extractDatePart(values[0], 'second');
82
+
83
+ case 'numberFormat':
84
+ return this.formatNumber(
85
+ values[0],
86
+ config.decimalPlaces,
87
+ config.prefix,
88
+ config.suffix
89
+ );
90
+
91
+ case 'template':
92
+ return this.applyTemplate(
93
+ config.template ?? '',
94
+ sourceFields,
95
+ sourceValues
96
+ );
97
+
98
+ default:
99
+ return String(values[0] ?? '');
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Apply a transformation to a single value (for chained transformations)
105
+ */
106
+ applyTransformationToValue(
107
+ value: unknown,
108
+ config: TransformationConfig
109
+ ): string {
110
+ const str = String(value ?? '');
111
+
112
+ switch (config.type) {
113
+ case 'direct':
114
+ return str;
115
+
116
+ case 'concat':
117
+ // For single value, just return it (concat needs multiple values)
118
+ return str;
119
+
120
+ case 'substring':
121
+ return str.substring(
122
+ config.startIndex ?? 0,
123
+ config.endIndex ?? str.length
124
+ );
125
+
126
+ case 'replace':
127
+ return str.replace(
128
+ new RegExp(config.searchValue ?? '', 'g'),
129
+ config.replaceValue ?? ''
130
+ );
131
+
132
+ case 'uppercase':
133
+ return str.toUpperCase();
134
+
135
+ case 'lowercase':
136
+ return str.toLowerCase();
137
+
138
+ case 'trim':
139
+ return str.trim();
140
+
141
+ case 'mask':
142
+ return this.applyMask(str, config.pattern ?? '');
143
+
144
+ case 'dateFormat':
145
+ return this.formatDate(value, config.inputFormat, config.outputFormat);
146
+
147
+ case 'extractYear':
148
+ return this.extractDatePart(value, 'year');
149
+
150
+ case 'extractMonth':
151
+ return this.extractDatePart(value, 'month');
152
+
153
+ case 'extractDay':
154
+ return this.extractDatePart(value, 'day');
155
+
156
+ case 'extractHour':
157
+ return this.extractDatePart(value, 'hour');
158
+
159
+ case 'extractMinute':
160
+ return this.extractDatePart(value, 'minute');
161
+
162
+ case 'extractSecond':
163
+ return this.extractDatePart(value, 'second');
164
+
165
+ case 'numberFormat':
166
+ return this.formatNumber(
167
+ value,
168
+ config.decimalPlaces,
169
+ config.prefix,
170
+ config.suffix
171
+ );
172
+
173
+ case 'template':
174
+ // For single value, replace {0} with the value
175
+ return (config.template ?? '').replace(/\{0\}/g, str);
176
+
177
+ default:
178
+ return str;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Apply multiple transformations in sequence, respecting conditions
184
+ */
185
+ applyTransformations(
186
+ sourceValues: Record<string, unknown>,
187
+ sourceFields: SchemaField[],
188
+ transformations: TransformationConfig[]
189
+ ): string {
190
+ if (transformations.length === 0) {
191
+ return '';
192
+ }
193
+
194
+ // Get initial value for first transformation
195
+ const initialValues = sourceFields.map((f) => this.getValueByPath(sourceValues, f.path));
196
+ const initialValue = initialValues.length === 1 ? initialValues[0] : initialValues.join(' ');
197
+
198
+ // Apply first transformation using source fields (check condition first)
199
+ let result: string;
200
+ if (this.isConditionMet(initialValue, transformations[0])) {
201
+ result = this.applyTransformation(sourceValues, sourceFields, transformations[0]);
202
+ } else {
203
+ result = String(initialValue ?? '');
204
+ }
205
+
206
+ // Apply subsequent transformations to the result
207
+ for (let i = 1; i < transformations.length; i++) {
208
+ if (this.isConditionMet(result, transformations[i])) {
209
+ result = this.applyTransformationToValue(result, transformations[i]);
210
+ }
211
+ // If condition not met, result passes through unchanged
212
+ }
213
+
214
+ return result;
215
+ }
216
+
217
+ private getValueByPath(obj: Record<string, unknown>, path: string): unknown {
218
+ return path.split('.').reduce((acc: unknown, part) => {
219
+ if (acc && typeof acc === 'object') {
220
+ return (acc as Record<string, unknown>)[part];
221
+ }
222
+ return undefined;
223
+ }, obj);
224
+ }
225
+
226
+ private applyTemplate(
227
+ template: string,
228
+ sourceFields: SchemaField[],
229
+ sourceValues: Record<string, unknown>
230
+ ): string {
231
+ let result = template;
232
+ // Replace positional placeholders {0}, {1}, etc.
233
+ sourceFields.forEach((field, index) => {
234
+ const value = this.getValueByPath(sourceValues, field.path);
235
+ result = result.replace(
236
+ new RegExp(`\\{${index}\\}`, 'g'),
237
+ String(value ?? '')
238
+ );
239
+ });
240
+ return result;
241
+ }
242
+
243
+ private formatDate(
244
+ value: unknown,
245
+ inputFormat?: string,
246
+ outputFormat?: string
247
+ ): string {
248
+ if (!value) return '';
249
+ try {
250
+ const date = new Date(value as string);
251
+ if (isNaN(date.getTime())) return String(value);
252
+
253
+ // Simple format implementation
254
+ const format = outputFormat ?? 'YYYY-MM-DD';
255
+ return format
256
+ .replace('YYYY', date.getFullYear().toString())
257
+ .replace('MM', (date.getMonth() + 1).toString().padStart(2, '0'))
258
+ .replace('DD', date.getDate().toString().padStart(2, '0'))
259
+ .replace('HH', date.getHours().toString().padStart(2, '0'))
260
+ .replace('mm', date.getMinutes().toString().padStart(2, '0'))
261
+ .replace('ss', date.getSeconds().toString().padStart(2, '0'));
262
+ } catch {
263
+ return String(value);
264
+ }
265
+ }
266
+
267
+ private extractDatePart(
268
+ value: unknown,
269
+ part: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'
270
+ ): string {
271
+ if (!value) return '';
272
+ try {
273
+ const date = new Date(value as string);
274
+ if (isNaN(date.getTime())) return String(value);
275
+
276
+ switch (part) {
277
+ case 'year':
278
+ return date.getFullYear().toString();
279
+ case 'month':
280
+ return (date.getMonth() + 1).toString().padStart(2, '0');
281
+ case 'day':
282
+ return date.getDate().toString().padStart(2, '0');
283
+ case 'hour':
284
+ return date.getHours().toString().padStart(2, '0');
285
+ case 'minute':
286
+ return date.getMinutes().toString().padStart(2, '0');
287
+ case 'second':
288
+ return date.getSeconds().toString().padStart(2, '0');
289
+ default:
290
+ return String(value);
291
+ }
292
+ } catch {
293
+ return String(value);
294
+ }
295
+ }
296
+
297
+ private formatNumber(
298
+ value: unknown,
299
+ decimalPlaces?: number,
300
+ prefix?: string,
301
+ suffix?: string
302
+ ): string {
303
+ if (value === null || value === undefined) return '';
304
+ const num = Number(value);
305
+ if (isNaN(num)) return String(value);
306
+
307
+ let formatted = decimalPlaces !== undefined
308
+ ? num.toFixed(decimalPlaces)
309
+ : num.toString();
310
+
311
+ return `${prefix ?? ''}${formatted}${suffix ?? ''}`;
312
+ }
313
+
314
+ private applyMask(input: string, pattern: string): string {
315
+ if (!pattern) return input;
316
+
317
+ let result = '';
318
+ let inputIndex = 0;
319
+
320
+ for (let i = 0; i < pattern.length; i++) {
321
+ const patternChar = pattern.charAt(i);
322
+ if (patternChar === '#') {
323
+ if (inputIndex < input.length) {
324
+ result += input.charAt(inputIndex);
325
+ inputIndex++;
326
+ }
327
+ } else {
328
+ result += patternChar;
329
+ }
330
+ }
331
+
332
+ return result;
333
+ }
334
+
335
+ getTransformationLabel(type: TransformationType): string {
336
+ const labels: Record<TransformationType, string> = {
337
+ direct: 'Direct Mapping',
338
+ concat: 'Concatenate',
339
+ substring: 'Substring',
340
+ replace: 'Find & Replace',
341
+ uppercase: 'Uppercase',
342
+ lowercase: 'Lowercase',
343
+ trim: 'Trim',
344
+ mask: 'Mask',
345
+ dateFormat: 'Date Format',
346
+ extractYear: 'Extract Year',
347
+ extractMonth: 'Extract Month',
348
+ extractDay: 'Extract Day',
349
+ extractHour: 'Extract Hour',
350
+ extractMinute: 'Extract Minute',
351
+ extractSecond: 'Extract Second',
352
+ numberFormat: 'Number Format',
353
+ template: 'Template',
354
+ };
355
+ return labels[type];
356
+ }
357
+
358
+ getAvailableTransformations(): { type: TransformationType; label: string; category?: string }[] {
359
+ return [
360
+ { type: 'direct', label: 'Direct Mapping' },
361
+ { type: 'concat', label: 'Concatenate', category: 'String' },
362
+ { type: 'substring', label: 'Substring', category: 'String' },
363
+ { type: 'replace', label: 'Find & Replace', category: 'String' },
364
+ { type: 'uppercase', label: 'Uppercase', category: 'String' },
365
+ { type: 'lowercase', label: 'Lowercase', category: 'String' },
366
+ { type: 'trim', label: 'Trim', category: 'String' },
367
+ { type: 'mask', label: 'Mask', category: 'String' },
368
+ { type: 'template', label: 'Template', category: 'String' },
369
+ { type: 'dateFormat', label: 'Format Date', category: 'Date' },
370
+ { type: 'extractYear', label: 'Extract Year', category: 'Date' },
371
+ { type: 'extractMonth', label: 'Extract Month', category: 'Date' },
372
+ { type: 'extractDay', label: 'Extract Day', category: 'Date' },
373
+ { type: 'extractHour', label: 'Extract Hour', category: 'Date' },
374
+ { type: 'extractMinute', label: 'Extract Minute', category: 'Date' },
375
+ { type: 'extractSecond', label: 'Extract Second', category: 'Date' },
376
+ { type: 'numberFormat', label: 'Number Format', category: 'Number' },
377
+ ];
378
+ }
379
+
380
+ // Condition evaluation methods
381
+ evaluateCondition(value: unknown, condition: FilterGroup): boolean {
382
+ return this.evaluateGroup(value, condition);
383
+ }
384
+
385
+ private evaluateGroup(value: unknown, group: FilterGroup): boolean {
386
+ if (group.children.length === 0) {
387
+ return true; // Empty group is always true
388
+ }
389
+
390
+ if (group.logic === 'and') {
391
+ return group.children.every(child => this.evaluateItem(value, child));
392
+ } else {
393
+ return group.children.some(child => this.evaluateItem(value, child));
394
+ }
395
+ }
396
+
397
+ private evaluateItem(value: unknown, item: FilterItem): boolean {
398
+ if (item.type === 'group') {
399
+ return this.evaluateGroup(value, item);
400
+ } else {
401
+ return this.evaluateConditionItem(value, item);
402
+ }
403
+ }
404
+
405
+ private evaluateConditionItem(value: unknown, condition: FilterCondition): boolean {
406
+ const strValue = String(value ?? '');
407
+ const numValue = Number(value);
408
+ const condValue = condition.value;
409
+
410
+ switch (condition.operator) {
411
+ case 'equals':
412
+ return strValue === String(condValue);
413
+ case 'notEquals':
414
+ return strValue !== String(condValue);
415
+ case 'contains':
416
+ return strValue.includes(String(condValue));
417
+ case 'notContains':
418
+ return !strValue.includes(String(condValue));
419
+ case 'startsWith':
420
+ return strValue.startsWith(String(condValue));
421
+ case 'endsWith':
422
+ return strValue.endsWith(String(condValue));
423
+ case 'isEmpty':
424
+ return strValue === '' || value === null || value === undefined;
425
+ case 'isNotEmpty':
426
+ return strValue !== '' && value !== null && value !== undefined;
427
+ case 'greaterThan':
428
+ return !isNaN(numValue) && numValue > Number(condValue);
429
+ case 'lessThan':
430
+ return !isNaN(numValue) && numValue < Number(condValue);
431
+ case 'greaterThanOrEqual':
432
+ return !isNaN(numValue) && numValue >= Number(condValue);
433
+ case 'lessThanOrEqual':
434
+ return !isNaN(numValue) && numValue <= Number(condValue);
435
+ case 'isTrue':
436
+ return value === true || strValue.toLowerCase() === 'true';
437
+ case 'isFalse':
438
+ return value === false || strValue.toLowerCase() === 'false';
439
+ default:
440
+ return true;
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Check if a transformation's condition is met
446
+ */
447
+ isConditionMet(value: unknown, config: TransformationConfig): boolean {
448
+ if (!config.condition?.enabled || !config.condition.root) {
449
+ return true; // No condition means always apply
450
+ }
451
+ return this.evaluateCondition(value, config.condition.root);
452
+ }
453
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Public API Surface of ngx-data-mapper
3
+ */
4
+
5
+ // Models
6
+ export * from './lib/models/schema.model';
7
+ export * from './lib/models/json-schema.model';
8
+
9
+ // Services
10
+ export * from './lib/services/mapping.service';
11
+ export * from './lib/services/transformation.service';
12
+ export * from './lib/services/svg-connector.service';
13
+ export * from './lib/services/schema-parser.service';
14
+
15
+ // Components
16
+ export * from './lib/components/data-mapper/data-mapper.component';
17
+ export * from './lib/components/schema-editor/schema-editor.component';
18
+ export * from './lib/components/schema-tree/schema-tree.component';
19
+ export * from './lib/components/transformation-popover/transformation-popover.component';
20
+ export * from './lib/components/array-filter-modal/array-filter-modal.component';
21
+ export * from './lib/components/array-selector-modal/array-selector-modal.component';
22
+ export * from './lib/components/default-value-popover/default-value-popover.component';
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../out-tsc/lib",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "inlineSources": true,
8
+ "types": []
9
+ },
10
+ "exclude": [
11
+ "**/*.spec.ts"
12
+ ]
13
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.lib.json",
3
+ "compilerOptions": {
4
+ "declarationMap": false
5
+ },
6
+ "angularCompilerOptions": {
7
+ "compilationMode": "partial"
8
+ }
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "strict": true,
6
+ "noImplicitOverride": true,
7
+ "noPropertyAccessFromIndexSignature": true,
8
+ "noImplicitReturns": true,
9
+ "noFallthroughCasesInSwitch": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "experimentalDecorators": true,
13
+ "importHelpers": true,
14
+ "target": "ES2022",
15
+ "module": "preserve",
16
+ "paths": {
17
+ "@expeed/ngx-data-mapper": [
18
+ "projects/ngx-data-mapper/src/public-api.ts"
19
+ ]
20
+ }
21
+ },
22
+ "angularCompilerOptions": {
23
+ "enableI18nLegacyMessageIdFormat": false,
24
+ "strictInjectionParameters": true,
25
+ "strictInputAccessModifiers": true,
26
+ "strictTemplates": true
27
+ }
28
+ }