@api-client/core 0.18.16 → 0.18.18

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 (56) hide show
  1. package/build/src/{modeling → decorators}/observed.d.ts +3 -3
  2. package/build/src/decorators/observed.d.ts.map +1 -0
  3. package/build/src/{modeling → decorators}/observed.js +4 -4
  4. package/build/src/decorators/observed.js.map +1 -0
  5. package/build/src/modeling/ApiModel.js +1 -1
  6. package/build/src/modeling/ApiModel.js.map +1 -1
  7. package/build/src/modeling/DataDomain.d.ts +7 -2
  8. package/build/src/modeling/DataDomain.d.ts.map +1 -1
  9. package/build/src/modeling/DataDomain.js +15 -2
  10. package/build/src/modeling/DataDomain.js.map +1 -1
  11. package/build/src/modeling/DomainAssociation.d.ts +7 -0
  12. package/build/src/modeling/DomainAssociation.d.ts.map +1 -1
  13. package/build/src/modeling/DomainAssociation.js +44 -1
  14. package/build/src/modeling/DomainAssociation.js.map +1 -1
  15. package/build/src/modeling/DomainEntity.d.ts +6 -0
  16. package/build/src/modeling/DomainEntity.d.ts.map +1 -1
  17. package/build/src/modeling/DomainEntity.js +21 -1
  18. package/build/src/modeling/DomainEntity.js.map +1 -1
  19. package/build/src/modeling/DomainModel.js +1 -1
  20. package/build/src/modeling/DomainModel.js.map +1 -1
  21. package/build/src/modeling/DomainNamespace.js +1 -1
  22. package/build/src/modeling/DomainNamespace.js.map +1 -1
  23. package/build/src/modeling/DomainProperty.d.ts +5 -0
  24. package/build/src/modeling/DomainProperty.d.ts.map +1 -1
  25. package/build/src/modeling/DomainProperty.js +38 -1
  26. package/build/src/modeling/DomainProperty.js.map +1 -1
  27. package/build/src/modeling/DomainSerialization.d.ts +6 -3
  28. package/build/src/modeling/DomainSerialization.d.ts.map +1 -1
  29. package/build/src/modeling/DomainSerialization.js +374 -52
  30. package/build/src/modeling/DomainSerialization.js.map +1 -1
  31. package/build/src/modeling/types.d.ts +69 -2
  32. package/build/src/modeling/types.d.ts.map +1 -1
  33. package/build/src/modeling/types.js.map +1 -1
  34. package/build/src/models/Thing.js +1 -1
  35. package/build/src/models/Thing.js.map +1 -1
  36. package/build/tsconfig.tsbuildinfo +1 -1
  37. package/data/models/example-generator-api.json +10 -10
  38. package/package.json +2 -1
  39. package/src/{modeling → decorators}/observed.ts +5 -5
  40. package/src/modeling/ApiModel.ts +1 -1
  41. package/src/modeling/DataDomain.ts +24 -3
  42. package/src/modeling/DomainAssociation.ts +51 -1
  43. package/src/modeling/DomainEntity.ts +24 -1
  44. package/src/modeling/DomainModel.ts +1 -1
  45. package/src/modeling/DomainNamespace.ts +1 -1
  46. package/src/modeling/DomainProperty.ts +43 -1
  47. package/src/modeling/DomainSerialization.ts +440 -56
  48. package/src/modeling/types.ts +73 -2
  49. package/src/models/Thing.ts +1 -1
  50. package/tests/unit/decorators/observed.spec.ts +527 -0
  51. package/tests/unit/modeling/data_domain_serialization.spec.ts +508 -0
  52. package/tests/unit/modeling/domain_asociation.spec.ts +376 -0
  53. package/tests/unit/modeling/domain_entity.spec.ts +147 -0
  54. package/tests/unit/modeling/domain_property.spec.ts +273 -0
  55. package/build/src/modeling/observed.d.ts.map +0 -1
  56. package/build/src/modeling/observed.js.map +0 -1
@@ -1,6 +1,15 @@
1
1
  import type { JsonEdge, Node } from '@api-client/graph/graph/types.js'
2
2
  import { Graph } from '@api-client/graph/graph/Graph.js'
3
- import type { DataDomainGraph, DomainGraphEdge, DomainGraphNodeType, SerializedGraph } from './types.js'
3
+ import type {
4
+ DataDomainGraph,
5
+ DomainGraphEdge,
6
+ DomainGraphNodeType,
7
+ SerializedGraph,
8
+ DeserializationMode,
9
+ DeserializationResult,
10
+ DeserializationIssue,
11
+ DeserializeOptions,
12
+ } from './types.js'
4
13
  import {
5
14
  DomainAssociationKind,
6
15
  DomainEntityKind,
@@ -78,7 +87,152 @@ function writeEdges(g: DataDomainGraph, domainKey: string): JsonEdge<DomainGraph
78
87
  return result
79
88
  }
80
89
 
90
+ /**
91
+ * Validates the graph consistency before serialization.
92
+ * Ensures that all required edges exist for properties and associations.
93
+ * @param g The graph to validate
94
+ * @param domainKey The domain key to validate
95
+ * @throws Error if the graph is inconsistent
96
+ */
97
+ function validateGraphConsistency(g: DataDomainGraph, domainKey: string): void {
98
+ const validationErrors: string[] = []
99
+
100
+ // Get all nodes that belong to this domain
101
+ const domainNodes = new Set<string>()
102
+ for (const nodeId of g.nodes()) {
103
+ const node = g.node(nodeId)
104
+ if (node && node.domain.key === domainKey) {
105
+ domainNodes.add(nodeId)
106
+ }
107
+ }
108
+
109
+ // Validate that properties and associations have parent edges
110
+ for (const nodeId of domainNodes) {
111
+ const node = g.node(nodeId)
112
+ if (!node) continue
113
+
114
+ if (node.kind === DomainPropertyKind || node.kind === DomainAssociationKind) {
115
+ // These nodes must have exactly one incoming edge from their parent entity
116
+ const incomingEdges = [...g.inEdges(nodeId)]
117
+ const parentEdges = incomingEdges.filter((edge) => {
118
+ const sourceNode = g.node(edge.v)
119
+ return sourceNode && sourceNode.domain.key === domainKey
120
+ })
121
+
122
+ if (parentEdges.length === 0) {
123
+ const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association'
124
+ validationErrors.push(
125
+ `${nodeType} "${node.info.name}" (${nodeId}) has no parent entity edge. ` +
126
+ `This will cause deserialization to fail.`
127
+ )
128
+ } else if (parentEdges.length > 1) {
129
+ const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association'
130
+ const parentIds = parentEdges.map((e) => e.v).join(', ')
131
+ validationErrors.push(
132
+ `${nodeType} "${node.info.name}" (${nodeId}) has multiple parent edges: ${parentIds}. ` +
133
+ `This may cause deserialization issues.`
134
+ )
135
+ } else {
136
+ // Validate that the parent is actually an entity
137
+ const parentEdge = parentEdges[0]
138
+ const parentNode = g.node(parentEdge.v)
139
+ if (!parentNode || parentNode.kind !== DomainEntityKind) {
140
+ const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association'
141
+ validationErrors.push(
142
+ `${nodeType} "${node.info.name}" (${nodeId}) has a parent that is not an entity. ` +
143
+ `Parent: ${parentNode?.kind || 'unknown'} (${parentEdge.v})`
144
+ )
145
+ }
146
+ }
147
+ }
148
+
149
+ // Validate that entities have model parents
150
+ if (node.kind === DomainEntityKind) {
151
+ const parents = [...g.parents(nodeId)]
152
+ const hasModelParent = parents.some((parentId) => {
153
+ const parent = g.node(parentId)
154
+ return parent && parent.kind === DomainModelKind
155
+ })
156
+
157
+ if (!hasModelParent) {
158
+ validationErrors.push(
159
+ `Entity "${node.info.name}" (${nodeId}) has no model parent. ` + `Entities must belong to a model.`
160
+ )
161
+ }
162
+ }
163
+
164
+ // Validate that models have either namespace parents or are at root level
165
+ if (node.kind === DomainModelKind) {
166
+ const parents = [...g.parents(nodeId)]
167
+ const hasNamespaceParent = parents.some((parentId) => {
168
+ const parent = g.node(parentId)
169
+ return parent && parent.kind === DomainNamespaceKind
170
+ })
171
+
172
+ // Models can either be at root level (no parents) or have a namespace parent
173
+ // They should not have other types of parents
174
+ if (parents.length > 0 && !hasNamespaceParent) {
175
+ const invalidParents = parents
176
+ .map((parentId) => {
177
+ const parent = g.node(parentId)
178
+ return `${parent?.kind || 'unknown'} (${parentId})`
179
+ })
180
+ .join(', ')
181
+ validationErrors.push(
182
+ `Model "${node.info.name}" (${nodeId}) has invalid parent types: ${invalidParents}. ` +
183
+ `Models can only belong to namespaces or be at root level.`
184
+ )
185
+ }
186
+ }
187
+ }
188
+
189
+ // Validate association targets exist
190
+ for (const nodeId of domainNodes) {
191
+ const node = g.node(nodeId)
192
+ if (node && node.kind === DomainAssociationKind) {
193
+ const association = node as DomainAssociation
194
+ const outgoingEdges = [...g.outEdges(nodeId)]
195
+
196
+ // Check that association has target edges
197
+ const targetEdges = outgoingEdges.filter((edge) => {
198
+ const edgeData = g.edge(edge)
199
+ return edgeData && edgeData.type === 'association'
200
+ })
201
+
202
+ if (targetEdges.length === 0) {
203
+ // This is general warning message, do not an error.
204
+ // We can serialize and deserialize associations without targets.
205
+ }
206
+
207
+ // Validate that all target entities exist
208
+ for (const targetEdge of targetEdges) {
209
+ const targetNode = g.node(targetEdge.w)
210
+ if (!targetNode) {
211
+ validationErrors.push(
212
+ `Association "${association.info.name}" (${nodeId}) references non-existent target: ${targetEdge.w}`
213
+ )
214
+ } else if (targetNode.kind !== DomainEntityKind) {
215
+ validationErrors.push(
216
+ `Association "${association.info.name}" (${nodeId}) references non-entity target: ` +
217
+ `${targetNode.kind} (${targetEdge.w})`
218
+ )
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ if (validationErrors.length > 0) {
225
+ throw new Error(
226
+ `Graph consistency validation failed for domain "${domainKey}":\n` +
227
+ validationErrors.map((error) => ` - ${error}`).join('\n')
228
+ )
229
+ }
230
+ }
231
+
81
232
  export function serialize(g: DataDomainGraph, domainKey: string): SerializedGraph {
233
+ // Validate graph consistency before serialization
234
+ validateGraphConsistency(g, domainKey)
235
+
82
236
  const json: SerializedGraph = {
83
237
  options: {
84
238
  directed: g.isDirected(),
@@ -198,43 +352,124 @@ function findEdgeParent(child: string, edges: JsonEdge<DomainGraphEdge>[]): stri
198
352
  * @param root The DataDomain instance to use as the root for the graph
199
353
  * @param entry The node entry to restore
200
354
  * @param edges The list of serialized graph edges
201
- * @returns The restored node instance
202
- * @throws Error if the entry is malformed or the kind is unknown
355
+ * @param mode The deserialization mode
356
+ * @param issues Array to collect issues in lenient mode
357
+ * @returns The restored node instance or undefined if failed in lenient mode
358
+ * @throws Error if the entry is malformed or the kind is unknown in strict mode
203
359
  */
204
- function prepareNode(root: DataDomain, entry: unknown, edges: JsonEdge<DomainGraphEdge>[]): DomainGraphNodeType {
360
+ function prepareNode(
361
+ root: DataDomain,
362
+ entry: unknown,
363
+ edges: JsonEdge<DomainGraphEdge>[],
364
+ mode: DeserializationMode,
365
+ issues: DeserializationIssue[]
366
+ ): DomainGraphNodeType | undefined {
205
367
  if (!entry) {
206
- throw new Error(`Unable to restore data domain graph. Malformed node entry`)
368
+ const issue: DeserializationIssue = {
369
+ type: 'malformed_entry',
370
+ severity: 'error',
371
+ message: 'Unable to restore data domain graph. Malformed node entry',
372
+ }
373
+ issues.push(issue)
374
+
375
+ if (mode === 'strict') {
376
+ throw new Error(issue.message)
377
+ }
378
+ return undefined
207
379
  }
208
- const domainElement = entry as { kind?: string }
380
+
381
+ const domainElement = entry as { kind?: string; key?: string }
209
382
  if (!domainElement.kind) {
210
- throw new Error(`Unable to restore data domain graph. Malformed node entry`)
211
- }
212
- if (domainElement.kind === DomainNamespaceKind) {
213
- return new DomainNamespace(root, domainElement as DomainNamespaceSchema)
214
- }
215
- if (domainElement.kind === DomainModelKind) {
216
- return new DomainModel(root, domainElement as DomainModelSchema)
217
- }
218
- if (domainElement.kind === DomainEntityKind) {
219
- return new DomainEntity(root, domainElement as DomainEntitySchema)
220
- }
221
- if (domainElement.kind === DomainPropertyKind) {
222
- const typed = domainElement as DomainPropertySchema
223
- const parent = findEdgeParent(typed.key, edges)
224
- if (!parent) {
225
- throw new Error(`Unable to restore data domain graph. Malformed node entry`)
383
+ const issue: DeserializationIssue = {
384
+ type: 'malformed_entry',
385
+ severity: 'error',
386
+ message: 'Unable to restore data domain graph. Node entry missing kind property',
387
+ affectedKey: domainElement.key,
226
388
  }
227
- return new DomainProperty(root, parent, typed)
389
+ issues.push(issue)
390
+
391
+ if (mode === 'strict') {
392
+ throw new Error(issue.message)
393
+ }
394
+ return undefined
228
395
  }
229
- if (domainElement.kind === DomainAssociationKind) {
230
- const typed = domainElement as DomainAssociationSchema
231
- const parent = findEdgeParent(typed.key, edges)
232
- if (!parent) {
233
- throw new Error(`Unable to restore data domain graph. Malformed node entry`)
396
+
397
+ try {
398
+ if (domainElement.kind === DomainNamespaceKind) {
399
+ return new DomainNamespace(root, domainElement as DomainNamespaceSchema)
400
+ }
401
+ if (domainElement.kind === DomainModelKind) {
402
+ return new DomainModel(root, domainElement as DomainModelSchema)
403
+ }
404
+ if (domainElement.kind === DomainEntityKind) {
405
+ return new DomainEntity(root, domainElement as DomainEntitySchema)
406
+ }
407
+ if (domainElement.kind === DomainPropertyKind) {
408
+ const typed = domainElement as DomainPropertySchema
409
+ const parent = findEdgeParent(typed.key, edges)
410
+ if (!parent) {
411
+ const issue: DeserializationIssue = {
412
+ type: 'missing_edge',
413
+ severity: 'error',
414
+ message: `Property "${typed.info?.name || typed.key}" has no parent entity edge`,
415
+ affectedKey: typed.key,
416
+ resolution: mode === 'lenient' ? 'Property will be skipped' : undefined,
417
+ }
418
+ issues.push(issue)
419
+
420
+ if (mode === 'strict') {
421
+ throw new Error(`Unable to restore data domain graph. Malformed node entry`)
422
+ }
423
+ return undefined
424
+ }
425
+ return new DomainProperty(root, parent, typed)
426
+ }
427
+ if (domainElement.kind === DomainAssociationKind) {
428
+ const typed = domainElement as DomainAssociationSchema
429
+ const parent = findEdgeParent(typed.key, edges)
430
+ if (!parent) {
431
+ const issue: DeserializationIssue = {
432
+ type: 'missing_edge',
433
+ severity: 'error',
434
+ message: `Association "${typed.info?.name || typed.key}" has no parent entity edge`,
435
+ affectedKey: typed.key,
436
+ resolution: mode === 'lenient' ? 'Association will be skipped' : undefined,
437
+ }
438
+ issues.push(issue)
439
+ if (mode === 'strict') {
440
+ throw new Error(`Unable to restore data domain graph. Malformed node entry`)
441
+ }
442
+ return undefined
443
+ }
444
+ return new DomainAssociation(root, parent, typed)
445
+ }
446
+
447
+ const issue: DeserializationIssue = {
448
+ type: 'unknown_kind',
449
+ severity: 'error',
450
+ message: `Unknown node kind: ${domainElement.kind}`,
451
+ affectedKey: domainElement.key,
234
452
  }
235
- return new DomainAssociation(root, parent, typed)
453
+ issues.push(issue)
454
+
455
+ if (mode === 'strict') {
456
+ throw new Error(`Unable to restore data domain graph. Unknown node kind ${domainElement.kind}`)
457
+ }
458
+ return undefined
459
+ } catch (error) {
460
+ const issue: DeserializationIssue = {
461
+ type: 'malformed_entry',
462
+ severity: 'error',
463
+ message: `Failed to create node: ${error instanceof Error ? error.message : String(error)}`,
464
+ affectedKey: domainElement.key,
465
+ }
466
+ issues.push(issue)
467
+
468
+ if (mode === 'strict') {
469
+ throw error
470
+ }
471
+ return undefined
236
472
  }
237
- throw new Error(`Unable to restore data domain graph. Unknown node kind ${domainElement.kind}`)
238
473
  }
239
474
 
240
475
  /**
@@ -246,74 +481,223 @@ function prepareNode(root: DataDomain, entry: unknown, edges: JsonEdge<DomainGra
246
481
  *
247
482
  * @param root The DataDomain instance to use as the root for the graph
248
483
  * @param json The previously serialized graph
249
- * @returns Deserialized graph instance
484
+ * @param dependencies An array of foreign data domains to register with this domain
485
+ * @param mode The deserialization mode - 'strict' throws errors, 'lenient' collects issues
486
+ * @returns DeserializationResult with graph, issues, and success status
487
+ * @throws Error in strict mode when any issue is encountered
250
488
  */
251
- export function deserialize(root: DataDomain, json?: SerializedGraph, dependencies?: DataDomain[]): DataDomainGraph {
489
+ export function deserialize(root: DataDomain, options: DeserializeOptions = {}): DeserializationResult {
490
+ const { json, dependencies, mode = 'strict' } = options
252
491
  const g = new Graph<unknown, DomainGraphNodeType, DomainGraphEdge>({
253
492
  compound: true,
254
493
  multigraph: true,
255
494
  directed: true,
256
495
  })
496
+ const issues: DeserializationIssue[] = []
257
497
  let foreignNodes = new Set<string>()
258
498
  let foreignEdges = new Set<string>()
499
+
500
+ // Process dependencies
259
501
  if (dependencies) {
260
502
  for (const dependency of dependencies) {
261
- const result = mergeGraph(g, dependency.graph, dependency.key)
262
- if (result.edges.size) {
263
- foreignEdges = new Set([...foreignEdges, ...result.edges])
264
- }
265
- if (result.nodes.size) {
266
- foreignNodes = new Set([...foreignNodes, ...result.nodes])
503
+ try {
504
+ const result = mergeGraph(g, dependency.graph, dependency.key)
505
+ if (result.edges.size) {
506
+ foreignEdges = new Set([...foreignEdges, ...result.edges])
507
+ }
508
+ if (result.nodes.size) {
509
+ foreignNodes = new Set([...foreignNodes, ...result.nodes])
510
+ }
511
+ root.dependencies.set(dependency.key, dependency)
512
+ } catch (error) {
513
+ const message = `Failed to merge dependency "${dependency.key}": ${error instanceof Error ? error.message : String(error)}`
514
+
515
+ if (mode === 'strict') {
516
+ throw new Error(message)
517
+ }
518
+
519
+ const issue: DeserializationIssue = {
520
+ type: 'missing_dependency',
521
+ severity: 'error',
522
+ message,
523
+ affectedKey: dependency.key,
524
+ resolution: 'Dependency will be skipped',
525
+ }
526
+ issues.push(issue)
267
527
  }
268
- root.dependencies.set(dependency.key, dependency)
269
528
  }
270
529
  }
530
+
271
531
  if (!json) {
272
- return g
532
+ return {
533
+ graph: g,
534
+ issues,
535
+ success: true,
536
+ }
273
537
  }
538
+
539
+ // Process nodes
274
540
  if (Array.isArray(json.nodes)) {
275
541
  // 1st pass - set up nodes
276
542
  const parentInfo = new Map<string, string[]>()
277
543
  for (const entry of json.nodes) {
278
- g.setNode(entry.v, prepareNode(root, entry.value, json.edges))
279
- if (entry.parents) {
280
- parentInfo.set(entry.v, entry.parents)
544
+ const node = prepareNode(root, entry.value, json.edges, mode, issues)
545
+ if (node) {
546
+ g.setNode(entry.v, node)
547
+ if (entry.parents) {
548
+ parentInfo.set(entry.v, entry.parents)
549
+ }
281
550
  }
551
+ // Note: prepareNode throws in strict mode, so we won't reach here if there's an error
282
552
  }
553
+
283
554
  // 2nd pass - set up parents
284
555
  for (const [key, parents] of parentInfo) {
285
- // In data domain graph, all nodes that can have parents can only have a single parent.
286
- // It's the business logic of the library.
287
- // Parent-child relationships:
288
- // - Namespace -> Namespace
289
- // - Namespace -> Model
290
- // - Model -> Entity
291
- // Entities and Association are associated with the parent entity through edges.
556
+ // Verify the node still exists (might have been skipped in lenient mode)
557
+ if (!g.hasNode(key)) {
558
+ const issue: DeserializationIssue = {
559
+ type: 'missing_node',
560
+ severity: 'warning',
561
+ message: `Cannot set parents for missing node: ${key}`,
562
+ affectedKey: key,
563
+ resolution: 'Parent relationship will be skipped',
564
+ }
565
+ issues.push(issue)
566
+ continue
567
+ }
568
+
292
569
  for (const parent of parents) {
293
- g.setParent(key, parent)
570
+ // Verify parent exists
571
+ if (!g.hasNode(parent)) {
572
+ const issue: DeserializationIssue = {
573
+ type: 'missing_node',
574
+ severity: 'warning',
575
+ message: `Parent node "${parent}" not found for child "${key}"`,
576
+ affectedKey: key,
577
+ context: { parentKey: parent },
578
+ resolution: 'Parent relationship will be skipped',
579
+ }
580
+ issues.push(issue)
581
+ continue
582
+ }
583
+
584
+ try {
585
+ g.setParent(key, parent)
586
+ } catch (error) {
587
+ const message = `Failed to set parent "${parent}" for child "${key}": ${error instanceof Error ? error.message : String(error)}`
588
+ const issue: DeserializationIssue = {
589
+ type: 'invalid_parent',
590
+ severity: 'warning',
591
+ message,
592
+ affectedKey: key,
593
+ context: { parentKey: parent },
594
+ resolution: 'Parent relationship will be skipped',
595
+ }
596
+ issues.push(issue)
597
+ }
294
598
  }
295
599
  }
296
600
  }
601
+
602
+ // Process edges
297
603
  if (Array.isArray(json.edges)) {
298
604
  for (const entry of json.edges) {
299
605
  if (!entry.value) {
300
- throw new Error(`Unable to restore data domain graph. Malformed edge entry`)
606
+ const message = 'Edge entry missing value'
607
+
608
+ if (mode === 'strict') {
609
+ throw new Error(message)
610
+ }
611
+
612
+ const issue: DeserializationIssue = {
613
+ type: 'malformed_entry',
614
+ severity: 'error',
615
+ message,
616
+ context: { edge: entry },
617
+ }
618
+ issues.push(issue)
619
+ continue
301
620
  }
621
+
622
+ // Handle foreign edges
302
623
  if (entry.value.foreign) {
303
624
  // The `v` has to be local to the graph. The `w` must be foreign.
304
625
  if (!foreignNodes.has(entry.w)) {
305
- // console.warn(`Missing foreign node: ${entry.w}. Skipping edge.`)
626
+ // Note, we don't consider this an error, just a warning.
627
+ // Because of that, we don't throw an error here.
628
+ const issue: DeserializationIssue = {
629
+ type: 'missing_node',
630
+ severity: 'warning',
631
+ message: `Missing foreign node: ${entry.w}`,
632
+ affectedKey: entry.w,
633
+ resolution: 'Edge will be skipped',
634
+ }
635
+ issues.push(issue)
306
636
  continue
307
637
  }
308
638
  }
639
+
640
+ // Check if nodes exist for the edge
309
641
  if (foreignNodes.has(entry.v) || foreignNodes.has(entry.w)) {
310
642
  if (!g.hasNode(entry.v) || !g.hasNode(entry.w)) {
311
- // console.warn(`Missing foreign node: ${entry.v} or ${entry.w}. Skipping edge.`)
643
+ const issue: DeserializationIssue = {
644
+ type: 'missing_node',
645
+ severity: 'warning',
646
+ message: `Missing foreign node for edge: ${entry.v} -> ${entry.w}`,
647
+ context: { sourceNode: entry.v, targetNode: entry.w },
648
+ resolution: 'Edge will be skipped',
649
+ }
650
+ issues.push(issue)
651
+ continue
652
+ }
653
+ } else {
654
+ // Both nodes should be local - verify they exist
655
+ if (!g.hasNode(entry.v)) {
656
+ const issue: DeserializationIssue = {
657
+ type: 'missing_node',
658
+ severity: 'warning',
659
+ message: `Source node not found for edge: ${entry.v}`,
660
+ affectedKey: entry.v,
661
+ resolution: 'Edge will be skipped',
662
+ }
663
+ issues.push(issue)
664
+ continue
665
+ }
666
+
667
+ if (!g.hasNode(entry.w)) {
668
+ const issue: DeserializationIssue = {
669
+ type: 'missing_node',
670
+ severity: 'warning',
671
+ message: `Target node not found for edge: ${entry.w}`,
672
+ affectedKey: entry.w,
673
+ resolution: 'Edge will be skipped',
674
+ }
675
+ issues.push(issue)
312
676
  continue
313
677
  }
314
678
  }
315
- g.setEdge({ v: entry.v, w: entry.w, name: entry.name }, entry.value)
679
+
680
+ try {
681
+ g.setEdge({ v: entry.v, w: entry.w, name: entry.name }, entry.value)
682
+ } catch (error) {
683
+ const message = `Failed to create edge ${entry.v} -> ${entry.w}: ${error instanceof Error ? error.message : String(error)}`
684
+ const issue: DeserializationIssue = {
685
+ type: 'malformed_entry',
686
+ severity: 'warning',
687
+ message,
688
+ context: { sourceNode: entry.v, targetNode: entry.w },
689
+ resolution: 'Edge will be skipped',
690
+ }
691
+ issues.push(issue)
692
+ }
316
693
  }
317
694
  }
318
- return g
695
+
696
+ const hasErrors = issues.some((issue) => issue.severity === 'error')
697
+
698
+ return {
699
+ graph: g,
700
+ issues,
701
+ success: !hasErrors,
702
+ }
319
703
  }
@@ -13,6 +13,7 @@ import type {
13
13
  DomainAssociationKind,
14
14
  DataDomainKind,
15
15
  } from '../models/kinds.js'
16
+ import type { DataDomain } from './DataDomain.js'
16
17
 
17
18
  export interface DataDomainRemoveOptions {
18
19
  /**
@@ -55,7 +56,7 @@ export interface DomainGraphEdge {
55
56
  /**
56
57
  * The type of the edge.
57
58
  * - `association` The edge is to an association object.
58
- * - When coming **from** an entiry (the `v` property), that entity owns the association.
59
+ * - When coming **from** an entity (the `v` property), that entity owns the association.
59
60
  * - When coming **to** an entity (the `w` property), that entity is the target of the association.
60
61
  * An association can have multiple targets.
61
62
  * - `property` The edge is to a property object. Can only be created between an entity and a property.
@@ -684,7 +685,7 @@ export interface MatchUserPropertyAccessRule extends BaseAccessRule {
684
685
  /**
685
686
  * The action is allowed if the authenticated user's email domain matches a specific domain.
686
687
  * This is used to restrict access based on the user's email address.
687
- * For example, only users with an email address from "mycompany.com" can access certain resources.
688
+ * For example, only users with an email address from "my-company.com" can access certain resources.
688
689
  */
689
690
  export interface MatchEmailDomainAccessRule extends BaseAccessRule {
690
691
  type: 'matchEmailDomain'
@@ -822,3 +823,73 @@ export interface DomainImpactItem {
822
823
  */
823
824
  parent?: string
824
825
  }
826
+
827
+ export interface DeserializeOptions {
828
+ /**
829
+ * The mode to use for deserialization.
830
+ */
831
+ mode?: DeserializationMode
832
+ /**
833
+ * The serialized graph to deserialize.
834
+ * This is the JSON representation of the graph.
835
+ */
836
+ json?: SerializedGraph
837
+ /**
838
+ * The list of foreign domains that this domain depends on.
839
+ */
840
+ dependencies?: DataDomain[]
841
+ }
842
+
843
+ /**
844
+ * Describes the mode for deserializing a domain graph.
845
+ */
846
+ export type DeserializationMode = 'strict' | 'lenient'
847
+
848
+ /**
849
+ * Describes an issue found during deserialization.
850
+ */
851
+ export interface DeserializationIssue {
852
+ /**
853
+ * The type of issue encountered.
854
+ */
855
+ type: 'missing_node' | 'missing_edge' | 'invalid_parent' | 'missing_dependency' | 'malformed_entry' | 'unknown_kind'
856
+ /**
857
+ * The severity of the issue.
858
+ */
859
+ severity: 'error' | 'warning' | 'info'
860
+ /**
861
+ * A human-readable description of the issue.
862
+ */
863
+ message: string
864
+ /**
865
+ * The key of the affected node, edge, or entity if applicable.
866
+ */
867
+ affectedKey?: string
868
+ /**
869
+ * Additional context about the issue.
870
+ */
871
+ context?: Record<string, unknown>
872
+ /**
873
+ * The action taken to handle this issue in lenient mode.
874
+ */
875
+ resolution?: string
876
+ }
877
+
878
+ /**
879
+ * The result of a deserialization operation.
880
+ */
881
+ export interface DeserializationResult {
882
+ /**
883
+ * The deserialized graph.
884
+ */
885
+ graph: DataDomainGraph
886
+ /**
887
+ * Issues encountered during deserialization.
888
+ */
889
+ issues: DeserializationIssue[]
890
+ /**
891
+ * Whether the deserialization was successful.
892
+ * This is set to true when a critical failures occurred.
893
+ */
894
+ success: boolean
895
+ }
@@ -1,6 +1,6 @@
1
1
  import { ThingKind } from './kinds.js'
2
2
  import { ModelValidationOptions } from './types.js'
3
- import { observed } from '../modeling/observed.js'
3
+ import { observed } from '../decorators/observed.js'
4
4
 
5
5
  /**
6
6
  * An interface describing a base metadata of a thing.