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,317 @@
1
+ /**
2
+ * Schema Editor Page Styles - Modern Professional Design
3
+ */
4
+
5
+ .page-container {
6
+ height: 100%;
7
+ display: flex;
8
+ flex-direction: column;
9
+ background: #f8fafc;
10
+ }
11
+
12
+ // Page Header
13
+ .page-header {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ padding: 20px 32px;
18
+ background: white;
19
+ border-bottom: 1px solid #e2e8f0;
20
+ flex-shrink: 0;
21
+
22
+ .header-left {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: 2px;
26
+ }
27
+
28
+ h1 {
29
+ font-size: 20px;
30
+ font-weight: 600;
31
+ color: #0f172a;
32
+ margin: 0;
33
+ letter-spacing: -0.3px;
34
+ }
35
+
36
+ .subtitle {
37
+ font-size: 13px;
38
+ color: #64748b;
39
+ margin: 0;
40
+ }
41
+ }
42
+
43
+ // Main Content Area
44
+ .page-main {
45
+ flex: 1;
46
+ display: flex;
47
+ min-height: 0;
48
+ overflow: hidden;
49
+ }
50
+
51
+ // Schema List Panel (Left Sidebar)
52
+ .list-panel {
53
+ width: 280px;
54
+ flex-shrink: 0;
55
+ background: white;
56
+ border-right: 1px solid #e2e8f0;
57
+ display: flex;
58
+ flex-direction: column;
59
+ overflow: hidden;
60
+ }
61
+
62
+ .list-header {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: space-between;
66
+ padding: 16px 20px;
67
+ border-bottom: 1px solid #f1f5f9;
68
+
69
+ .list-title {
70
+ font-size: 12px;
71
+ font-weight: 600;
72
+ color: #64748b;
73
+ text-transform: uppercase;
74
+ letter-spacing: 0.5px;
75
+ }
76
+
77
+ .add-schema-btn {
78
+ font-size: 11px;
79
+ font-weight: 500;
80
+ padding: 0 12px;
81
+ min-width: auto;
82
+ height: 28px;
83
+ line-height: 28px;
84
+ border-radius: 14px;
85
+ }
86
+ }
87
+
88
+ .list-content {
89
+ flex: 1;
90
+ overflow-y: auto;
91
+ min-height: 0;
92
+ }
93
+
94
+ .empty-list {
95
+ display: flex;
96
+ flex-direction: column;
97
+ align-items: center;
98
+ justify-content: center;
99
+ padding: 48px 24px;
100
+ color: #94a3b8;
101
+ text-align: center;
102
+
103
+ mat-icon {
104
+ font-size: 40px;
105
+ width: 40px;
106
+ height: 40px;
107
+ margin-bottom: 12px;
108
+ opacity: 0.4;
109
+ }
110
+
111
+ p {
112
+ margin: 0 0 16px 0;
113
+ font-size: 13px;
114
+ }
115
+
116
+ button {
117
+ font-size: 13px;
118
+
119
+ mat-icon {
120
+ font-size: 16px;
121
+ width: 16px;
122
+ height: 16px;
123
+ margin-right: 4px;
124
+ }
125
+ }
126
+ }
127
+
128
+ .schema-list {
129
+ padding: 8px 12px;
130
+ }
131
+
132
+ .schema-item {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 10px;
136
+ padding: 10px 12px;
137
+ border-radius: 8px;
138
+ cursor: pointer;
139
+ transition: all 0.12s ease;
140
+ margin-bottom: 2px;
141
+ border: 1px solid transparent;
142
+
143
+ &:hover {
144
+ background: #f8fafc;
145
+
146
+ .schema-menu-btn {
147
+ opacity: 1;
148
+ }
149
+ }
150
+
151
+ &.active {
152
+ background: #f0f9ff;
153
+ border-color: #bae6fd;
154
+
155
+ .schema-icon {
156
+ color: #0284c7;
157
+ }
158
+
159
+ .schema-name {
160
+ color: #0369a1;
161
+ }
162
+ }
163
+
164
+ .schema-icon {
165
+ color: #94a3b8;
166
+ font-size: 18px;
167
+ width: 18px;
168
+ height: 18px;
169
+ flex-shrink: 0;
170
+ }
171
+
172
+ .schema-info {
173
+ flex: 1;
174
+ min-width: 0;
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 1px;
178
+ }
179
+
180
+ .schema-name {
181
+ font-size: 13px;
182
+ font-weight: 500;
183
+ color: #1e293b;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ .schema-meta {
190
+ font-size: 11px;
191
+ color: #94a3b8;
192
+ }
193
+
194
+ .schema-menu-btn {
195
+ opacity: 0;
196
+ transition: opacity 0.12s ease;
197
+ color: #94a3b8;
198
+ width: 28px;
199
+ height: 28px;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+
204
+ mat-icon {
205
+ font-size: 18px;
206
+ width: 18px;
207
+ height: 18px;
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ }
212
+ }
213
+ }
214
+
215
+ .list-footer {
216
+ padding: 12px 16px;
217
+ border-top: 1px solid #f1f5f9;
218
+
219
+ .import-btn {
220
+ width: 100%;
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: center;
224
+ gap: 6px;
225
+ font-size: 13px;
226
+ color: #64748b;
227
+ border-color: #e2e8f0;
228
+
229
+ mat-icon {
230
+ font-size: 16px;
231
+ width: 16px;
232
+ height: 16px;
233
+ }
234
+
235
+ &:hover {
236
+ background: #f8fafc;
237
+ border-color: #cbd5e1;
238
+ }
239
+ }
240
+ }
241
+
242
+ // Editor Panel (Right Side)
243
+ .editor-panel {
244
+ flex: 1;
245
+ display: flex;
246
+ flex-direction: column;
247
+ min-width: 0;
248
+ overflow: hidden;
249
+ background: #f8fafc;
250
+ padding: 24px;
251
+ gap: 16px;
252
+
253
+ schema-editor {
254
+ flex: 1;
255
+ display: flex;
256
+ flex-direction: column;
257
+ min-height: 0;
258
+ background: white;
259
+ border-radius: 12px;
260
+ border: 1px solid #e2e8f0;
261
+ overflow: hidden;
262
+
263
+ // Theme the schema-editor component
264
+ --schema-editor-bg: white;
265
+ --schema-editor-shadow: none;
266
+ --schema-editor-border-radius: 0;
267
+ --schema-editor-accent-primary: #6366f1;
268
+ --schema-editor-header-bg: #fafafa;
269
+ }
270
+ }
271
+
272
+ .no-selection {
273
+ flex: 1;
274
+ display: flex;
275
+ flex-direction: column;
276
+ align-items: center;
277
+ justify-content: center;
278
+ color: #94a3b8;
279
+ text-align: center;
280
+ background: #f8fafc;
281
+
282
+ mat-icon {
283
+ font-size: 48px;
284
+ width: 48px;
285
+ height: 48px;
286
+ margin-bottom: 16px;
287
+ opacity: 0.3;
288
+ }
289
+
290
+ p {
291
+ font-size: 14px;
292
+ margin: 0;
293
+ color: #64748b;
294
+ }
295
+ }
296
+
297
+ .delete-action {
298
+ color: #ef4444 !important;
299
+
300
+ mat-icon {
301
+ color: #ef4444;
302
+ }
303
+ }
304
+
305
+ // Responsive
306
+ @media (max-width: 768px) {
307
+ .page-main {
308
+ flex-direction: column;
309
+ }
310
+
311
+ .list-panel {
312
+ width: 100%;
313
+ max-height: 200px;
314
+ border-right: none;
315
+ border-bottom: 1px solid #e2e8f0;
316
+ }
317
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Schema Editor Page - Example usage of the SchemaEditorComponent
3
+ */
4
+ import { Component, inject, signal, computed } from '@angular/core';
5
+ import { CommonModule } from '@angular/common';
6
+ import { MatButtonModule } from '@angular/material/button';
7
+ import { MatIconModule } from '@angular/material/icon';
8
+ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
9
+ import { MatTooltipModule } from '@angular/material/tooltip';
10
+ import { MatMenuModule } from '@angular/material/menu';
11
+ import { SchemaEditorComponent, JsonSchema } from '@expeed/ngx-data-mapper';
12
+ import { AppStateService, StoredSchema } from '../../services/app-state.service';
13
+
14
+ @Component({
15
+ selector: 'schema-editor-page',
16
+ standalone: true,
17
+ imports: [
18
+ CommonModule,
19
+ MatButtonModule,
20
+ MatIconModule,
21
+ MatSnackBarModule,
22
+ MatTooltipModule,
23
+ MatMenuModule,
24
+ SchemaEditorComponent,
25
+ ],
26
+ templateUrl: './schema-editor-page.component.html',
27
+ styleUrl: './schema-editor-page.component.scss',
28
+ })
29
+ export class SchemaEditorPageComponent {
30
+ private snackBar = inject(MatSnackBar);
31
+ appState = inject(AppStateService);
32
+
33
+ // Currently selected schema
34
+ selectedSchemaId = signal<string | null>(null);
35
+
36
+ // Computed: get the currently selected schema
37
+ selectedSchema = computed(() => {
38
+ const id = this.selectedSchemaId();
39
+ return this.appState.schemas().find(s => s.id === id) || null;
40
+ });
41
+
42
+ // --- Schema CRUD Operations ---
43
+
44
+ createNewSchema(): void {
45
+ const newSchema = this.appState.addSchema({
46
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
47
+ type: 'object',
48
+ title: 'NewSchema',
49
+ properties: {},
50
+ required: [],
51
+ });
52
+ this.selectedSchemaId.set(newSchema.id);
53
+ }
54
+
55
+ selectSchema(id: string): void {
56
+ this.selectedSchemaId.set(id);
57
+ }
58
+
59
+ onSchemaChange(updated: JsonSchema): void {
60
+ const id = this.selectedSchemaId();
61
+ if (!id) return;
62
+ this.appState.updateSchema(id, updated);
63
+ }
64
+
65
+ duplicateSchema(schema: StoredSchema): void {
66
+ const newSchema = this.appState.addSchema({
67
+ ...JSON.parse(JSON.stringify(schema)),
68
+ title: (schema.title || 'Schema') + '_copy',
69
+ });
70
+ this.selectedSchemaId.set(newSchema.id);
71
+ this.snackBar.open('Schema duplicated', 'Close', { duration: 2000 });
72
+ }
73
+
74
+ deleteSchema(id: string): void {
75
+ this.appState.deleteSchema(id);
76
+ if (this.selectedSchemaId() === id) {
77
+ this.selectedSchemaId.set(null);
78
+ }
79
+ this.snackBar.open('Schema deleted', 'Close', { duration: 2000 });
80
+ }
81
+
82
+ // --- Export/Import ---
83
+
84
+ exportSchema(schema: StoredSchema): void {
85
+ const { id, ...jsonSchema } = schema;
86
+ const json = JSON.stringify(jsonSchema, null, 2);
87
+ const blob = new Blob([json], { type: 'application/json' });
88
+ const url = URL.createObjectURL(blob);
89
+
90
+ const link = document.createElement('a');
91
+ link.href = url;
92
+ link.download = `${(schema.title || 'schema').toLowerCase()}.schema.json`;
93
+ link.click();
94
+
95
+ URL.revokeObjectURL(url);
96
+ this.snackBar.open('Schema exported as JSON Schema', 'Close', { duration: 2000 });
97
+ }
98
+
99
+ importSchema(event: Event): void {
100
+ const input = event.target as HTMLInputElement;
101
+ if (!input.files?.length) return;
102
+
103
+ const file = input.files[0];
104
+ const reader = new FileReader();
105
+
106
+ reader.onload = () => {
107
+ try {
108
+ const json = reader.result as string;
109
+ const data = JSON.parse(json) as JsonSchema;
110
+
111
+ const newSchema = this.appState.addSchema({
112
+ ...data,
113
+ title: data.title || 'ImportedSchema',
114
+ });
115
+ this.selectedSchemaId.set(newSchema.id);
116
+ this.snackBar.open('Schema imported', 'Close', { duration: 2000 });
117
+ } catch (error) {
118
+ this.snackBar.open('Failed to import: invalid file', 'Close', { duration: 3000 });
119
+ }
120
+ };
121
+
122
+ reader.readAsText(file);
123
+ input.value = '';
124
+ }
125
+
126
+ getPropertyCount(schema: StoredSchema): number {
127
+ return Object.keys(schema.properties || {}).length;
128
+ }
129
+ }
@@ -0,0 +1,233 @@
1
+ import { Injectable, signal, computed } from '@angular/core';
2
+ import { JsonSchema } from '@expeed/ngx-data-mapper';
3
+
4
+ export interface StoredSchema extends JsonSchema {
5
+ id: string;
6
+ }
7
+
8
+ export interface StoredMapping {
9
+ id: string;
10
+ name: string;
11
+ sourceSchemaId: string;
12
+ targetSchemaId: string;
13
+ mappingData?: unknown; // The actual mapping configuration
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
17
+
18
+ @Injectable({
19
+ providedIn: 'root',
20
+ })
21
+ export class AppStateService {
22
+ // Schemas state
23
+ private _schemas = signal<StoredSchema[]>([]);
24
+ schemas = this._schemas.asReadonly();
25
+
26
+ // Mappings state
27
+ private _mappings = signal<StoredMapping[]>([]);
28
+ mappings = this._mappings.asReadonly();
29
+
30
+ constructor() {
31
+ this.loadFromStorage();
32
+ }
33
+
34
+ // --- Storage ---
35
+
36
+ private loadFromStorage(): void {
37
+ // Load schemas
38
+ const savedSchemas = localStorage.getItem('objectSchemas');
39
+ if (savedSchemas) {
40
+ try {
41
+ this._schemas.set(JSON.parse(savedSchemas));
42
+ } catch (e) {
43
+ console.error('Failed to load schemas:', e);
44
+ }
45
+ }
46
+
47
+ // If no schemas, add default sample schemas
48
+ if (this._schemas().length === 0) {
49
+ this.addDefaultSchemas();
50
+ }
51
+
52
+ // Load mappings
53
+ const savedMappings = localStorage.getItem('dataMappings');
54
+ if (savedMappings) {
55
+ try {
56
+ this._mappings.set(JSON.parse(savedMappings));
57
+ } catch (e) {
58
+ console.error('Failed to load mappings:', e);
59
+ }
60
+ }
61
+ }
62
+
63
+ // Reset to default schemas (clears existing and adds defaults)
64
+ resetToDefaults(): void {
65
+ localStorage.removeItem('objectSchemas');
66
+ localStorage.removeItem('dataMappings');
67
+ this._schemas.set([]);
68
+ this._mappings.set([]);
69
+ this.addDefaultSchemas();
70
+ }
71
+
72
+ private addDefaultSchemas(): void {
73
+ // Customer schema (source)
74
+ this.addSchema({
75
+ type: 'object',
76
+ title: 'Customer',
77
+ properties: {
78
+ firstName: { type: 'string', description: 'Customer first name' },
79
+ lastName: { type: 'string', description: 'Customer last name' },
80
+ middleName: { type: 'string', description: 'Customer middle name' },
81
+ prefix: { type: 'string', description: 'Name prefix (Mr, Mrs, Dr)' },
82
+ suffix: { type: 'string', description: 'Name suffix (Jr, Sr, III)' },
83
+ email: { type: 'string', description: 'Email address' },
84
+ secondaryEmail: { type: 'string', description: 'Secondary email' },
85
+ phone: { type: 'string', description: 'Phone number with area code' },
86
+ mobilePhone: { type: 'string', description: 'Mobile phone number' },
87
+ birthDate: { type: 'string', description: 'Date of birth' },
88
+ gender: { type: 'string', description: 'Gender' },
89
+ language: { type: 'string', description: 'Preferred language' },
90
+ address: {
91
+ type: 'object',
92
+ description: 'Customer address',
93
+ properties: {
94
+ street: { type: 'string', description: 'Street address' },
95
+ city: { type: 'string', description: 'City name' },
96
+ state: { type: 'string', description: 'State code' },
97
+ zipCode: { type: 'string', description: 'Postal code' },
98
+ country: { type: 'string', description: 'Country code' },
99
+ },
100
+ },
101
+ accountBalance: { type: 'number', description: 'Current account balance' },
102
+ loyaltyPoints: { type: 'number', description: 'Loyalty points balance' },
103
+ isActive: { type: 'boolean', description: 'Account active status' },
104
+ isVerified: { type: 'boolean', description: 'Email verified status' },
105
+ },
106
+ });
107
+
108
+ // UserProfile schema (target)
109
+ this.addSchema({
110
+ type: 'object',
111
+ title: 'UserProfile',
112
+ properties: {
113
+ fullName: { type: 'string', description: 'Full display name' },
114
+ displayName: { type: 'string', description: 'Public display name' },
115
+ emailAddress: { type: 'string', description: 'Email address' },
116
+ alternateEmail: { type: 'string', description: 'Alternate email' },
117
+ phoneNumber: { type: 'string', description: 'Phone number' },
118
+ mobileNumber: { type: 'string', description: 'Mobile number' },
119
+ birthYear: { type: 'string', description: 'Birth year' },
120
+ age: { type: 'number', description: 'Calculated age' },
121
+ preferredLanguage: { type: 'string', description: 'Preferred language' },
122
+ location: {
123
+ type: 'object',
124
+ description: 'User location',
125
+ properties: {
126
+ fullAddress: { type: 'string', description: 'Combined street and city' },
127
+ region: { type: 'string', description: 'State or region' },
128
+ postalCode: { type: 'string', description: 'Postal code' },
129
+ country: { type: 'string', description: 'Country' },
130
+ },
131
+ },
132
+ balance: { type: 'string', description: 'Formatted balance with currency' },
133
+ rewardPoints: { type: 'number', description: 'Reward points' },
134
+ status: { type: 'string', description: 'Account status' },
135
+ isEmailVerified: { type: 'boolean', description: 'Email verified' },
136
+ hasOptedInMarketing: { type: 'boolean', description: 'Marketing opt-in' },
137
+ },
138
+ });
139
+ }
140
+
141
+ private saveSchemas(): void {
142
+ localStorage.setItem('objectSchemas', JSON.stringify(this._schemas()));
143
+ }
144
+
145
+ private saveMappings(): void {
146
+ localStorage.setItem('dataMappings', JSON.stringify(this._mappings()));
147
+ }
148
+
149
+ // --- Schema Operations ---
150
+
151
+ private generateId(prefix: string): string {
152
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
153
+ }
154
+
155
+ getSchemaById(id: string): StoredSchema | undefined {
156
+ return this._schemas().find(s => s.id === id);
157
+ }
158
+
159
+ addSchema(schema: Omit<StoredSchema, 'id'>): StoredSchema {
160
+ const newSchema: StoredSchema = {
161
+ ...schema,
162
+ id: this.generateId('schema'),
163
+ };
164
+ this._schemas.update(list => [...list, newSchema]);
165
+ this.saveSchemas();
166
+ return newSchema;
167
+ }
168
+
169
+ updateSchema(id: string, schema: Partial<JsonSchema>): void {
170
+ this._schemas.update(list =>
171
+ list.map(s => {
172
+ if (s.id !== id) return s;
173
+ // Create new schema with only the id preserved, replacing all other properties
174
+ // This ensures removed properties (like empty 'required' array) are actually removed
175
+ const { id: schemaId } = s;
176
+ return { id: schemaId, ...schema } as StoredSchema;
177
+ })
178
+ );
179
+ this.saveSchemas();
180
+ }
181
+
182
+ deleteSchema(id: string): void {
183
+ this._schemas.update(list => list.filter(s => s.id !== id));
184
+ this.saveSchemas();
185
+ }
186
+
187
+ setSchemas(schemas: StoredSchema[]): void {
188
+ this._schemas.set(schemas);
189
+ this.saveSchemas();
190
+ }
191
+
192
+ // --- Mapping Operations ---
193
+
194
+ getMappingById(id: string): StoredMapping | undefined {
195
+ return this._mappings().find(m => m.id === id);
196
+ }
197
+
198
+ addMapping(mapping: Omit<StoredMapping, 'id' | 'createdAt' | 'updatedAt'>): StoredMapping {
199
+ const now = new Date().toISOString();
200
+ const newMapping: StoredMapping = {
201
+ ...mapping,
202
+ id: this.generateId('mapping'),
203
+ createdAt: now,
204
+ updatedAt: now,
205
+ };
206
+ this._mappings.update(list => [...list, newMapping]);
207
+ this.saveMappings();
208
+ return newMapping;
209
+ }
210
+
211
+ updateMapping(id: string, mapping: Partial<StoredMapping>): void {
212
+ this._mappings.update(list =>
213
+ list.map(m => m.id === id ? { ...m, ...mapping, updatedAt: new Date().toISOString() } : m)
214
+ );
215
+ this.saveMappings();
216
+ }
217
+
218
+ updateMappingData(id: string, mappingData: unknown): void {
219
+ this.updateMapping(id, { mappingData });
220
+ }
221
+
222
+ deleteMapping(id: string): void {
223
+ this._mappings.update(list => list.filter(m => m.id !== id));
224
+ this.saveMappings();
225
+ }
226
+
227
+ // --- Helpers ---
228
+
229
+ getSchemaName(id: string): string {
230
+ const schema = this.getSchemaById(id);
231
+ return schema?.title || 'Unknown';
232
+ }
233
+ }