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,270 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { SchemaField, SchemaDefinition } from '../models/schema.model';
3
+ import { JsonSchema } from '../models/json-schema.model';
4
+
5
+ // Extended schema document with filtering options
6
+ export interface SchemaDocument extends JsonSchema {
7
+ $ref?: string;
8
+ $defs?: Record<string, JsonSchema>; // Alternative to definitions
9
+ exclude?: string[];
10
+ include?: string[];
11
+ }
12
+
13
+ export interface ModelRegistry {
14
+ [modelName: string]: JsonSchema;
15
+ }
16
+
17
+ @Injectable({
18
+ providedIn: 'root',
19
+ })
20
+ export class SchemaParserService {
21
+ private modelRegistry: ModelRegistry = {};
22
+ private idCounter = 0;
23
+
24
+ registerModels(models: ModelRegistry): void {
25
+ this.modelRegistry = { ...this.modelRegistry, ...models };
26
+ }
27
+
28
+ clearRegistry(): void {
29
+ this.modelRegistry = {};
30
+ }
31
+
32
+ parseSchema(
33
+ schemaJson: string | SchemaDocument,
34
+ schemaName: string = 'Schema'
35
+ ): SchemaDefinition {
36
+ const schema: SchemaDocument =
37
+ typeof schemaJson === 'string' ? JSON.parse(schemaJson) : schemaJson;
38
+
39
+ this.idCounter = 0;
40
+
41
+ // Extract definitions from the schema document itself
42
+ const localDefs = schema.$defs || schema.definitions || {};
43
+ const combinedRegistry = { ...this.modelRegistry, ...localDefs };
44
+
45
+ let resolvedSchema: JsonSchema;
46
+
47
+ if (schema.$ref) {
48
+ // Resolve the reference
49
+ resolvedSchema = this.resolveRef(schema.$ref, combinedRegistry);
50
+ } else if (schema.properties) {
51
+ // Direct schema with properties
52
+ resolvedSchema = schema as JsonSchema;
53
+ } else {
54
+ throw new Error('Schema must have either $ref or properties');
55
+ }
56
+
57
+ // Build fields from the resolved schema
58
+ let fields = this.buildFields(resolvedSchema, combinedRegistry, '');
59
+
60
+ // Apply exclude filter
61
+ if (schema.exclude && schema.exclude.length > 0) {
62
+ fields = this.applyExclude(fields, schema.exclude);
63
+ }
64
+
65
+ // Apply include filter (only if specified)
66
+ if (schema.include && schema.include.length > 0) {
67
+ fields = this.applyInclude(fields, schema.include);
68
+ }
69
+
70
+ return {
71
+ name: schema.title || schemaName,
72
+ fields,
73
+ };
74
+ }
75
+
76
+ private resolveRef(
77
+ ref: string,
78
+ registry: Record<string, JsonSchema>
79
+ ): JsonSchema {
80
+ // Handle different ref formats:
81
+ // #model, #/definitions/model, #/$defs/model, model
82
+ let modelName: string;
83
+
84
+ if (ref.startsWith('#/$defs/')) {
85
+ modelName = ref.substring(8);
86
+ } else if (ref.startsWith('#/definitions/')) {
87
+ modelName = ref.substring(14);
88
+ } else if (ref.startsWith('#')) {
89
+ modelName = ref.substring(1);
90
+ } else {
91
+ modelName = ref;
92
+ }
93
+
94
+ const resolved = registry[modelName];
95
+ if (!resolved) {
96
+ throw new Error(`Cannot resolve reference: ${ref}. Model "${modelName}" not found in registry.`);
97
+ }
98
+
99
+ // If the resolved schema also has a $ref, resolve it recursively
100
+ if (resolved.$ref) {
101
+ return this.resolveRef(resolved.$ref, registry);
102
+ }
103
+
104
+ return resolved;
105
+ }
106
+
107
+ private buildFields(
108
+ schema: JsonSchema,
109
+ registry: Record<string, JsonSchema>,
110
+ parentPath: string,
111
+ arrayContext?: { isArrayItem: boolean; parentArrayPath: string }
112
+ ): SchemaField[] {
113
+ const fields: SchemaField[] = [];
114
+
115
+ if (!schema.properties) {
116
+ return fields;
117
+ }
118
+
119
+ for (const [name, propSchema] of Object.entries(schema.properties)) {
120
+ const path = parentPath ? `${parentPath}.${name}` : name;
121
+ const field = this.buildField(name, propSchema, registry, path, arrayContext);
122
+ fields.push(field);
123
+ }
124
+
125
+ return fields;
126
+ }
127
+
128
+ private buildField(
129
+ name: string,
130
+ schema: JsonSchema,
131
+ registry: Record<string, JsonSchema>,
132
+ path: string,
133
+ arrayContext?: { isArrayItem: boolean; parentArrayPath: string }
134
+ ): SchemaField {
135
+ // Resolve $ref if present
136
+ let resolvedSchema = schema;
137
+ if (schema.$ref) {
138
+ resolvedSchema = { ...this.resolveRef(schema.$ref, registry), ...schema };
139
+ delete resolvedSchema.$ref;
140
+ }
141
+
142
+ const fieldType = this.mapType(resolvedSchema);
143
+ const field: SchemaField = {
144
+ id: `field-${++this.idCounter}-${name}`,
145
+ name,
146
+ type: fieldType,
147
+ path,
148
+ description: resolvedSchema.description,
149
+ isArrayItem: arrayContext?.isArrayItem,
150
+ parentArrayPath: arrayContext?.parentArrayPath,
151
+ };
152
+
153
+ // Handle nested objects
154
+ if (fieldType === 'object' && resolvedSchema.properties) {
155
+ field.children = this.buildFields(resolvedSchema, registry, path, arrayContext);
156
+ field.expanded = true;
157
+ }
158
+
159
+ // Handle arrays with object items
160
+ if (fieldType === 'array' && resolvedSchema.items) {
161
+ let itemSchema = resolvedSchema.items;
162
+ if (itemSchema.$ref) {
163
+ itemSchema = this.resolveRef(itemSchema.$ref, registry);
164
+ }
165
+ if (itemSchema.properties) {
166
+ // Mark children as array items with reference to parent array
167
+ field.children = this.buildFields(itemSchema, registry, `${path}[]`, {
168
+ isArrayItem: true,
169
+ parentArrayPath: path,
170
+ });
171
+ field.expanded = true;
172
+ }
173
+ }
174
+
175
+ return field;
176
+ }
177
+
178
+ private mapType(
179
+ schema: JsonSchema
180
+ ): 'string' | 'number' | 'boolean' | 'object' | 'array' | 'date' {
181
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
182
+ const format = schema.format;
183
+
184
+ // Check format first for date types
185
+ if (format === 'date' || format === 'date-time' || format === 'time') {
186
+ return 'date';
187
+ }
188
+
189
+ switch (type) {
190
+ case 'string':
191
+ return 'string';
192
+ case 'number':
193
+ case 'integer':
194
+ return 'number';
195
+ case 'boolean':
196
+ return 'boolean';
197
+ case 'object':
198
+ return 'object';
199
+ case 'array':
200
+ return 'array';
201
+ default:
202
+ // If type is not specified but has properties, it's an object
203
+ if (schema.properties) {
204
+ return 'object';
205
+ }
206
+ return 'string';
207
+ }
208
+ }
209
+
210
+ private applyExclude(fields: SchemaField[], exclude: string[]): SchemaField[] {
211
+ return fields
212
+ .filter((field) => !exclude.includes(field.name) && !exclude.includes(field.path))
213
+ .map((field) => {
214
+ if (field.children) {
215
+ // Filter nested fields by checking both full path and relative name
216
+ const filteredChildren = this.applyExclude(field.children, exclude);
217
+ return { ...field, children: filteredChildren };
218
+ }
219
+ return field;
220
+ });
221
+ }
222
+
223
+ private applyInclude(fields: SchemaField[], include: string[]): SchemaField[] {
224
+ return fields
225
+ .filter((field) => {
226
+ // Include if field name or path matches
227
+ if (include.includes(field.name) || include.includes(field.path)) {
228
+ return true;
229
+ }
230
+ // Include if any child path matches
231
+ if (field.children) {
232
+ return this.hasIncludedChild(field.children, include);
233
+ }
234
+ return false;
235
+ })
236
+ .map((field) => {
237
+ if (field.children) {
238
+ // Keep parent but filter children
239
+ const filteredChildren = this.applyInclude(field.children, include);
240
+ return { ...field, children: filteredChildren.length > 0 ? filteredChildren : field.children };
241
+ }
242
+ return field;
243
+ });
244
+ }
245
+
246
+ private hasIncludedChild(fields: SchemaField[], include: string[]): boolean {
247
+ return fields.some((field) => {
248
+ if (include.includes(field.name) || include.includes(field.path)) {
249
+ return true;
250
+ }
251
+ if (field.children) {
252
+ return this.hasIncludedChild(field.children, include);
253
+ }
254
+ return false;
255
+ });
256
+ }
257
+
258
+ // Utility method to create a schema document from model name
259
+ createSchemaFromRef(
260
+ modelRef: string,
261
+ options?: { exclude?: string[]; include?: string[]; title?: string }
262
+ ): SchemaDocument {
263
+ return {
264
+ $ref: modelRef,
265
+ title: options?.title,
266
+ exclude: options?.exclude,
267
+ include: options?.include,
268
+ };
269
+ }
270
+ }
@@ -0,0 +1,135 @@
1
+ import { Injectable } from '@angular/core';
2
+
3
+ export interface Point {
4
+ x: number;
5
+ y: number;
6
+ }
7
+
8
+ export interface ConnectionPath {
9
+ id: string;
10
+ mappingId: string;
11
+ path: string;
12
+ sourcePoints: Point[];
13
+ targetPoint: Point;
14
+ midPoint: Point;
15
+ isSelected: boolean;
16
+ hasTransformation: boolean;
17
+ }
18
+
19
+ @Injectable({
20
+ providedIn: 'root',
21
+ })
22
+ export class SvgConnectorService {
23
+ createBezierPath(start: Point, end: Point): string {
24
+ const dx = end.x - start.x;
25
+ const controlPointOffset = Math.min(Math.abs(dx) * 0.5, 150);
26
+
27
+ const cp1x = start.x + controlPointOffset;
28
+ const cp1y = start.y;
29
+ const cp2x = end.x - controlPointOffset;
30
+ const cp2y = end.y;
31
+
32
+ return `M ${start.x} ${start.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${end.x} ${end.y}`;
33
+ }
34
+
35
+ createMultiSourcePath(sources: Point[], target: Point): { paths: string[]; mergePoint: Point } {
36
+ const mergeX = target.x - 80;
37
+ const mergeY = target.y;
38
+ const mergePoint = { x: mergeX, y: mergeY };
39
+
40
+ const paths = sources.map((source) => {
41
+ // Path from source to merge point
42
+ const dx1 = mergeX - source.x;
43
+ const cp1Offset = Math.min(Math.abs(dx1) * 0.4, 100);
44
+
45
+ return `M ${source.x} ${source.y} C ${source.x + cp1Offset} ${source.y}, ${mergeX - cp1Offset} ${mergeY}, ${mergeX} ${mergeY}`;
46
+ });
47
+
48
+ // Add path from merge point to target
49
+ const finalPath = `M ${mergeX} ${mergeY} L ${target.x} ${target.y}`;
50
+ paths.push(finalPath);
51
+
52
+ return { paths, mergePoint };
53
+ }
54
+
55
+ getMidPoint(start: Point, end: Point): Point {
56
+ return {
57
+ x: (start.x + end.x) / 2,
58
+ y: (start.y + end.y) / 2,
59
+ };
60
+ }
61
+
62
+ getMultiSourceMidPoint(sources: Point[], target: Point): Point {
63
+ const mergeX = target.x - 80;
64
+ return {
65
+ x: mergeX,
66
+ y: target.y,
67
+ };
68
+ }
69
+
70
+ calculateConnectionPoint(
71
+ rect: DOMRect,
72
+ side: 'source' | 'target',
73
+ containerRect: DOMRect
74
+ ): Point {
75
+ const relativeY = rect.top - containerRect.top + rect.height / 2;
76
+ // Offset to position endpoint circles outside the schema panels
77
+ const endpointOffset = 8;
78
+
79
+ if (side === 'source') {
80
+ return {
81
+ x: rect.right - containerRect.left + endpointOffset,
82
+ y: relativeY,
83
+ };
84
+ } else {
85
+ return {
86
+ x: rect.left - containerRect.left - endpointOffset,
87
+ y: relativeY,
88
+ };
89
+ }
90
+ }
91
+
92
+ isPointNearPath(
93
+ point: Point,
94
+ pathStart: Point,
95
+ pathEnd: Point,
96
+ threshold: number = 10
97
+ ): boolean {
98
+ // Simplified hit detection using distance to line segment
99
+ const A = point.x - pathStart.x;
100
+ const B = point.y - pathStart.y;
101
+ const C = pathEnd.x - pathStart.x;
102
+ const D = pathEnd.y - pathStart.y;
103
+
104
+ const dot = A * C + B * D;
105
+ const lenSq = C * C + D * D;
106
+ let param = -1;
107
+
108
+ if (lenSq !== 0) {
109
+ param = dot / lenSq;
110
+ }
111
+
112
+ let xx: number, yy: number;
113
+
114
+ if (param < 0) {
115
+ xx = pathStart.x;
116
+ yy = pathStart.y;
117
+ } else if (param > 1) {
118
+ xx = pathEnd.x;
119
+ yy = pathEnd.y;
120
+ } else {
121
+ xx = pathStart.x + param * C;
122
+ yy = pathStart.y + param * D;
123
+ }
124
+
125
+ const dx = point.x - xx;
126
+ const dy = point.y - yy;
127
+ const distance = Math.sqrt(dx * dx + dy * dy);
128
+
129
+ return distance <= threshold;
130
+ }
131
+
132
+ createDragPath(start: Point, end: Point): string {
133
+ return this.createBezierPath(start, end);
134
+ }
135
+ }