@api-client/core 0.18.16 → 0.18.17

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.
@@ -55,7 +55,125 @@ function writeEdges(g, domainKey) {
55
55
  }
56
56
  return result;
57
57
  }
58
+ /**
59
+ * Validates the graph consistency before serialization.
60
+ * Ensures that all required edges exist for properties and associations.
61
+ * @param g The graph to validate
62
+ * @param domainKey The domain key to validate
63
+ * @throws Error if the graph is inconsistent
64
+ */
65
+ function validateGraphConsistency(g, domainKey) {
66
+ const validationErrors = [];
67
+ // Get all nodes that belong to this domain
68
+ const domainNodes = new Set();
69
+ for (const nodeId of g.nodes()) {
70
+ const node = g.node(nodeId);
71
+ if (node && node.domain.key === domainKey) {
72
+ domainNodes.add(nodeId);
73
+ }
74
+ }
75
+ // Validate that properties and associations have parent edges
76
+ for (const nodeId of domainNodes) {
77
+ const node = g.node(nodeId);
78
+ if (!node)
79
+ continue;
80
+ if (node.kind === DomainPropertyKind || node.kind === DomainAssociationKind) {
81
+ // These nodes must have exactly one incoming edge from their parent entity
82
+ const incomingEdges = [...g.inEdges(nodeId)];
83
+ const parentEdges = incomingEdges.filter((edge) => {
84
+ const sourceNode = g.node(edge.v);
85
+ return sourceNode && sourceNode.domain.key === domainKey;
86
+ });
87
+ if (parentEdges.length === 0) {
88
+ const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association';
89
+ validationErrors.push(`${nodeType} "${node.info.name}" (${nodeId}) has no parent entity edge. ` +
90
+ `This will cause deserialization to fail.`);
91
+ }
92
+ else if (parentEdges.length > 1) {
93
+ const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association';
94
+ const parentIds = parentEdges.map((e) => e.v).join(', ');
95
+ validationErrors.push(`${nodeType} "${node.info.name}" (${nodeId}) has multiple parent edges: ${parentIds}. ` +
96
+ `This may cause deserialization issues.`);
97
+ }
98
+ else {
99
+ // Validate that the parent is actually an entity
100
+ const parentEdge = parentEdges[0];
101
+ const parentNode = g.node(parentEdge.v);
102
+ if (!parentNode || parentNode.kind !== DomainEntityKind) {
103
+ const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association';
104
+ validationErrors.push(`${nodeType} "${node.info.name}" (${nodeId}) has a parent that is not an entity. ` +
105
+ `Parent: ${parentNode?.kind || 'unknown'} (${parentEdge.v})`);
106
+ }
107
+ }
108
+ }
109
+ // Validate that entities have model parents
110
+ if (node.kind === DomainEntityKind) {
111
+ const parents = [...g.parents(nodeId)];
112
+ const hasModelParent = parents.some((parentId) => {
113
+ const parent = g.node(parentId);
114
+ return parent && parent.kind === DomainModelKind;
115
+ });
116
+ if (!hasModelParent) {
117
+ validationErrors.push(`Entity "${node.info.name}" (${nodeId}) has no model parent. ` + `Entities must belong to a model.`);
118
+ }
119
+ }
120
+ // Validate that models have either namespace parents or are at root level
121
+ if (node.kind === DomainModelKind) {
122
+ const parents = [...g.parents(nodeId)];
123
+ const hasNamespaceParent = parents.some((parentId) => {
124
+ const parent = g.node(parentId);
125
+ return parent && parent.kind === DomainNamespaceKind;
126
+ });
127
+ // Models can either be at root level (no parents) or have a namespace parent
128
+ // They should not have other types of parents
129
+ if (parents.length > 0 && !hasNamespaceParent) {
130
+ const invalidParents = parents
131
+ .map((parentId) => {
132
+ const parent = g.node(parentId);
133
+ return `${parent?.kind || 'unknown'} (${parentId})`;
134
+ })
135
+ .join(', ');
136
+ validationErrors.push(`Model "${node.info.name}" (${nodeId}) has invalid parent types: ${invalidParents}. ` +
137
+ `Models can only belong to namespaces or be at root level.`);
138
+ }
139
+ }
140
+ }
141
+ // Validate association targets exist
142
+ for (const nodeId of domainNodes) {
143
+ const node = g.node(nodeId);
144
+ if (node && node.kind === DomainAssociationKind) {
145
+ const association = node;
146
+ const outgoingEdges = [...g.outEdges(nodeId)];
147
+ // Check that association has target edges
148
+ const targetEdges = outgoingEdges.filter((edge) => {
149
+ const edgeData = g.edge(edge);
150
+ return edgeData && edgeData.type === 'association';
151
+ });
152
+ if (targetEdges.length === 0) {
153
+ validationErrors.push(`Association "${association.info.name}" (${nodeId}) has no target entities. ` +
154
+ `Associations must reference at least one target entity.`);
155
+ }
156
+ // Validate that all target entities exist
157
+ for (const targetEdge of targetEdges) {
158
+ const targetNode = g.node(targetEdge.w);
159
+ if (!targetNode) {
160
+ validationErrors.push(`Association "${association.info.name}" (${nodeId}) references non-existent target: ${targetEdge.w}`);
161
+ }
162
+ else if (targetNode.kind !== DomainEntityKind) {
163
+ validationErrors.push(`Association "${association.info.name}" (${nodeId}) references non-entity target: ` +
164
+ `${targetNode.kind} (${targetEdge.w})`);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ if (validationErrors.length > 0) {
170
+ throw new Error(`Graph consistency validation failed for domain "${domainKey}":\n` +
171
+ validationErrors.map((error) => ` - ${error}`).join('\n'));
172
+ }
173
+ }
58
174
  export function serialize(g, domainKey) {
175
+ // Validate graph consistency before serialization
176
+ validateGraphConsistency(g, domainKey);
59
177
  const json = {
60
178
  options: {
61
179
  directed: g.isDirected(),
@@ -173,43 +291,111 @@ function findEdgeParent(child, edges) {
173
291
  * @param root The DataDomain instance to use as the root for the graph
174
292
  * @param entry The node entry to restore
175
293
  * @param edges The list of serialized graph edges
176
- * @returns The restored node instance
177
- * @throws Error if the entry is malformed or the kind is unknown
294
+ * @param mode The deserialization mode
295
+ * @param issues Array to collect issues in lenient mode
296
+ * @returns The restored node instance or undefined if failed in lenient mode
297
+ * @throws Error if the entry is malformed or the kind is unknown in strict mode
178
298
  */
179
- function prepareNode(root, entry, edges) {
299
+ function prepareNode(root, entry, edges, mode, issues) {
180
300
  if (!entry) {
181
- throw new Error(`Unable to restore data domain graph. Malformed node entry`);
301
+ const issue = {
302
+ type: 'malformed_entry',
303
+ severity: 'error',
304
+ message: 'Unable to restore data domain graph. Malformed node entry',
305
+ };
306
+ issues.push(issue);
307
+ if (mode === 'strict') {
308
+ throw new Error(issue.message);
309
+ }
310
+ return undefined;
182
311
  }
183
312
  const domainElement = entry;
184
313
  if (!domainElement.kind) {
185
- throw new Error(`Unable to restore data domain graph. Malformed node entry`);
186
- }
187
- if (domainElement.kind === DomainNamespaceKind) {
188
- return new DomainNamespace(root, domainElement);
189
- }
190
- if (domainElement.kind === DomainModelKind) {
191
- return new DomainModel(root, domainElement);
192
- }
193
- if (domainElement.kind === DomainEntityKind) {
194
- return new DomainEntity(root, domainElement);
314
+ const issue = {
315
+ type: 'malformed_entry',
316
+ severity: 'error',
317
+ message: 'Unable to restore data domain graph. Node entry missing kind property',
318
+ affectedKey: domainElement.key,
319
+ };
320
+ issues.push(issue);
321
+ if (mode === 'strict') {
322
+ throw new Error(issue.message);
323
+ }
324
+ return undefined;
195
325
  }
196
- if (domainElement.kind === DomainPropertyKind) {
197
- const typed = domainElement;
198
- const parent = findEdgeParent(typed.key, edges);
199
- if (!parent) {
200
- throw new Error(`Unable to restore data domain graph. Malformed node entry`);
326
+ try {
327
+ if (domainElement.kind === DomainNamespaceKind) {
328
+ return new DomainNamespace(root, domainElement);
201
329
  }
202
- return new DomainProperty(root, parent, typed);
330
+ if (domainElement.kind === DomainModelKind) {
331
+ return new DomainModel(root, domainElement);
332
+ }
333
+ if (domainElement.kind === DomainEntityKind) {
334
+ return new DomainEntity(root, domainElement);
335
+ }
336
+ if (domainElement.kind === DomainPropertyKind) {
337
+ const typed = domainElement;
338
+ const parent = findEdgeParent(typed.key, edges);
339
+ if (!parent) {
340
+ const issue = {
341
+ type: 'missing_edge',
342
+ severity: 'error',
343
+ message: `Property "${typed.info?.name || typed.key}" has no parent entity edge`,
344
+ affectedKey: typed.key,
345
+ resolution: mode === 'lenient' ? 'Property will be skipped' : undefined,
346
+ };
347
+ issues.push(issue);
348
+ if (mode === 'strict') {
349
+ throw new Error(`Unable to restore data domain graph. Malformed node entry`);
350
+ }
351
+ return undefined;
352
+ }
353
+ return new DomainProperty(root, parent, typed);
354
+ }
355
+ if (domainElement.kind === DomainAssociationKind) {
356
+ const typed = domainElement;
357
+ const parent = findEdgeParent(typed.key, edges);
358
+ if (!parent) {
359
+ const issue = {
360
+ type: 'missing_edge',
361
+ severity: 'error',
362
+ message: `Association "${typed.info?.name || typed.key}" has no parent entity edge`,
363
+ affectedKey: typed.key,
364
+ resolution: mode === 'lenient' ? 'Association will be skipped' : undefined,
365
+ };
366
+ issues.push(issue);
367
+ if (mode === 'strict') {
368
+ throw new Error(`Unable to restore data domain graph. Malformed node entry`);
369
+ }
370
+ return undefined;
371
+ }
372
+ return new DomainAssociation(root, parent, typed);
373
+ }
374
+ const issue = {
375
+ type: 'unknown_kind',
376
+ severity: 'error',
377
+ message: `Unknown node kind: ${domainElement.kind}`,
378
+ affectedKey: domainElement.key,
379
+ };
380
+ issues.push(issue);
381
+ if (mode === 'strict') {
382
+ throw new Error(`Unable to restore data domain graph. Unknown node kind ${domainElement.kind}`);
383
+ }
384
+ return undefined;
203
385
  }
204
- if (domainElement.kind === DomainAssociationKind) {
205
- const typed = domainElement;
206
- const parent = findEdgeParent(typed.key, edges);
207
- if (!parent) {
208
- throw new Error(`Unable to restore data domain graph. Malformed node entry`);
386
+ catch (error) {
387
+ const issue = {
388
+ type: 'malformed_entry',
389
+ severity: 'error',
390
+ message: `Failed to create node: ${error instanceof Error ? error.message : String(error)}`,
391
+ affectedKey: domainElement.key,
392
+ };
393
+ issues.push(issue);
394
+ if (mode === 'strict') {
395
+ throw error;
209
396
  }
210
- return new DomainAssociation(root, parent, typed);
397
+ return undefined;
211
398
  }
212
- throw new Error(`Unable to restore data domain graph. Unknown node kind ${domainElement.kind}`);
213
399
  }
214
400
  /**
215
401
  * To deserialize a graph:
@@ -220,75 +406,211 @@ function prepareNode(root, entry, edges) {
220
406
  *
221
407
  * @param root The DataDomain instance to use as the root for the graph
222
408
  * @param json The previously serialized graph
223
- * @returns Deserialized graph instance
409
+ * @param dependencies An array of foreign data domains to register with this domain
410
+ * @param mode The deserialization mode - 'strict' throws errors, 'lenient' collects issues
411
+ * @returns DeserializationResult with graph, issues, and success status
412
+ * @throws Error in strict mode when any issue is encountered
224
413
  */
225
- export function deserialize(root, json, dependencies) {
414
+ export function deserialize(root, options = {}) {
415
+ const { json, dependencies, mode = 'strict' } = options;
226
416
  const g = new Graph({
227
417
  compound: true,
228
418
  multigraph: true,
229
419
  directed: true,
230
420
  });
421
+ const issues = [];
231
422
  let foreignNodes = new Set();
232
423
  let foreignEdges = new Set();
424
+ // Process dependencies
233
425
  if (dependencies) {
234
426
  for (const dependency of dependencies) {
235
- const result = mergeGraph(g, dependency.graph, dependency.key);
236
- if (result.edges.size) {
237
- foreignEdges = new Set([...foreignEdges, ...result.edges]);
427
+ try {
428
+ const result = mergeGraph(g, dependency.graph, dependency.key);
429
+ if (result.edges.size) {
430
+ foreignEdges = new Set([...foreignEdges, ...result.edges]);
431
+ }
432
+ if (result.nodes.size) {
433
+ foreignNodes = new Set([...foreignNodes, ...result.nodes]);
434
+ }
435
+ root.dependencies.set(dependency.key, dependency);
238
436
  }
239
- if (result.nodes.size) {
240
- foreignNodes = new Set([...foreignNodes, ...result.nodes]);
437
+ catch (error) {
438
+ const message = `Failed to merge dependency "${dependency.key}": ${error instanceof Error ? error.message : String(error)}`;
439
+ if (mode === 'strict') {
440
+ throw new Error(message);
441
+ }
442
+ const issue = {
443
+ type: 'missing_dependency',
444
+ severity: 'error',
445
+ message,
446
+ affectedKey: dependency.key,
447
+ resolution: 'Dependency will be skipped',
448
+ };
449
+ issues.push(issue);
241
450
  }
242
- root.dependencies.set(dependency.key, dependency);
243
451
  }
244
452
  }
245
453
  if (!json) {
246
- return g;
454
+ return {
455
+ graph: g,
456
+ issues,
457
+ success: true,
458
+ };
247
459
  }
460
+ // Process nodes
248
461
  if (Array.isArray(json.nodes)) {
249
462
  // 1st pass - set up nodes
250
463
  const parentInfo = new Map();
251
464
  for (const entry of json.nodes) {
252
- g.setNode(entry.v, prepareNode(root, entry.value, json.edges));
253
- if (entry.parents) {
254
- parentInfo.set(entry.v, entry.parents);
465
+ const node = prepareNode(root, entry.value, json.edges, mode, issues);
466
+ if (node) {
467
+ g.setNode(entry.v, node);
468
+ if (entry.parents) {
469
+ parentInfo.set(entry.v, entry.parents);
470
+ }
255
471
  }
472
+ // Note: prepareNode throws in strict mode, so we won't reach here if there's an error
256
473
  }
257
474
  // 2nd pass - set up parents
258
475
  for (const [key, parents] of parentInfo) {
259
- // In data domain graph, all nodes that can have parents can only have a single parent.
260
- // It's the business logic of the library.
261
- // Parent-child relationships:
262
- // - Namespace -> Namespace
263
- // - Namespace -> Model
264
- // - Model -> Entity
265
- // Entities and Association are associated with the parent entity through edges.
476
+ // Verify the node still exists (might have been skipped in lenient mode)
477
+ if (!g.hasNode(key)) {
478
+ const issue = {
479
+ type: 'missing_node',
480
+ severity: 'warning',
481
+ message: `Cannot set parents for missing node: ${key}`,
482
+ affectedKey: key,
483
+ resolution: 'Parent relationship will be skipped',
484
+ };
485
+ issues.push(issue);
486
+ continue;
487
+ }
266
488
  for (const parent of parents) {
267
- g.setParent(key, parent);
489
+ // Verify parent exists
490
+ if (!g.hasNode(parent)) {
491
+ const issue = {
492
+ type: 'missing_node',
493
+ severity: 'warning',
494
+ message: `Parent node "${parent}" not found for child "${key}"`,
495
+ affectedKey: key,
496
+ context: { parentKey: parent },
497
+ resolution: 'Parent relationship will be skipped',
498
+ };
499
+ issues.push(issue);
500
+ continue;
501
+ }
502
+ try {
503
+ g.setParent(key, parent);
504
+ }
505
+ catch (error) {
506
+ const message = `Failed to set parent "${parent}" for child "${key}": ${error instanceof Error ? error.message : String(error)}`;
507
+ const issue = {
508
+ type: 'invalid_parent',
509
+ severity: 'warning',
510
+ message,
511
+ affectedKey: key,
512
+ context: { parentKey: parent },
513
+ resolution: 'Parent relationship will be skipped',
514
+ };
515
+ issues.push(issue);
516
+ }
268
517
  }
269
518
  }
270
519
  }
520
+ // Process edges
271
521
  if (Array.isArray(json.edges)) {
272
522
  for (const entry of json.edges) {
273
523
  if (!entry.value) {
274
- throw new Error(`Unable to restore data domain graph. Malformed edge entry`);
524
+ const message = 'Edge entry missing value';
525
+ if (mode === 'strict') {
526
+ throw new Error(message);
527
+ }
528
+ const issue = {
529
+ type: 'malformed_entry',
530
+ severity: 'error',
531
+ message,
532
+ context: { edge: entry },
533
+ };
534
+ issues.push(issue);
535
+ continue;
275
536
  }
537
+ // Handle foreign edges
276
538
  if (entry.value.foreign) {
277
539
  // The `v` has to be local to the graph. The `w` must be foreign.
278
540
  if (!foreignNodes.has(entry.w)) {
279
- // console.warn(`Missing foreign node: ${entry.w}. Skipping edge.`)
541
+ // Note, we don't consider this an error, just a warning.
542
+ // Because of that, we don't throw an error here.
543
+ const issue = {
544
+ type: 'missing_node',
545
+ severity: 'warning',
546
+ message: `Missing foreign node: ${entry.w}`,
547
+ affectedKey: entry.w,
548
+ resolution: 'Edge will be skipped',
549
+ };
550
+ issues.push(issue);
280
551
  continue;
281
552
  }
282
553
  }
554
+ // Check if nodes exist for the edge
283
555
  if (foreignNodes.has(entry.v) || foreignNodes.has(entry.w)) {
284
556
  if (!g.hasNode(entry.v) || !g.hasNode(entry.w)) {
285
- // console.warn(`Missing foreign node: ${entry.v} or ${entry.w}. Skipping edge.`)
557
+ const issue = {
558
+ type: 'missing_node',
559
+ severity: 'warning',
560
+ message: `Missing foreign node for edge: ${entry.v} -> ${entry.w}`,
561
+ context: { sourceNode: entry.v, targetNode: entry.w },
562
+ resolution: 'Edge will be skipped',
563
+ };
564
+ issues.push(issue);
565
+ continue;
566
+ }
567
+ }
568
+ else {
569
+ // Both nodes should be local - verify they exist
570
+ if (!g.hasNode(entry.v)) {
571
+ const issue = {
572
+ type: 'missing_node',
573
+ severity: 'warning',
574
+ message: `Source node not found for edge: ${entry.v}`,
575
+ affectedKey: entry.v,
576
+ resolution: 'Edge will be skipped',
577
+ };
578
+ issues.push(issue);
579
+ continue;
580
+ }
581
+ if (!g.hasNode(entry.w)) {
582
+ const issue = {
583
+ type: 'missing_node',
584
+ severity: 'warning',
585
+ message: `Target node not found for edge: ${entry.w}`,
586
+ affectedKey: entry.w,
587
+ resolution: 'Edge will be skipped',
588
+ };
589
+ issues.push(issue);
286
590
  continue;
287
591
  }
288
592
  }
289
- g.setEdge({ v: entry.v, w: entry.w, name: entry.name }, entry.value);
593
+ try {
594
+ g.setEdge({ v: entry.v, w: entry.w, name: entry.name }, entry.value);
595
+ }
596
+ catch (error) {
597
+ const message = `Failed to create edge ${entry.v} -> ${entry.w}: ${error instanceof Error ? error.message : String(error)}`;
598
+ const issue = {
599
+ type: 'malformed_entry',
600
+ severity: 'warning',
601
+ message,
602
+ context: { sourceNode: entry.v, targetNode: entry.w },
603
+ resolution: 'Edge will be skipped',
604
+ };
605
+ issues.push(issue);
606
+ }
290
607
  }
291
608
  }
292
- return g;
609
+ const hasErrors = issues.some((issue) => issue.severity === 'error');
610
+ return {
611
+ graph: g,
612
+ issues,
613
+ success: !hasErrors,
614
+ };
293
615
  }
294
616
  //# sourceMappingURL=DomainSerialization.js.map