@api-client/core 0.18.15 → 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.
@@ -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
+ }
@@ -392,3 +392,507 @@ test.group('DataDomain Serialization and Deserialization', () => {
392
392
  )
393
393
  }).tags(['@modeling', '@serialization'])
394
394
  })
395
+
396
+ test.group('Validation Tests', () => {
397
+ test('should throw validation error when property has no parent entity edge', ({ assert }) => {
398
+ const domain = new DataDomain()
399
+ const m1 = domain.addModel()
400
+ const e1 = m1.addEntity()
401
+ const p1 = e1.addProperty({ type: 'string' })
402
+
403
+ // Manually break the graph by removing the property edge
404
+ domain.graph.removeEdge(e1.key, p1.key)
405
+
406
+ assert.throws(() => domain.toJSON())
407
+ }).tags(['@modeling', '@serialization', '@validation'])
408
+
409
+ test('should throw validation error when association has no parent entity edge', ({ assert }) => {
410
+ const domain = new DataDomain()
411
+ const m1 = domain.addModel()
412
+ const e1 = m1.addEntity()
413
+ const e2 = m1.addEntity()
414
+ const a1 = e1.addAssociation({ key: e2.key })
415
+
416
+ // Manually break the graph by removing the association edge to parent
417
+ domain.graph.removeEdge(e1.key, a1.key)
418
+
419
+ assert.throws(() => domain.toJSON())
420
+ }).tags(['@modeling', '@serialization', '@validation'])
421
+
422
+ test('should throw validation error when property has multiple parent edges', ({ assert }) => {
423
+ const domain = new DataDomain()
424
+ const m1 = domain.addModel()
425
+ const e1 = m1.addEntity()
426
+ const e2 = m1.addEntity()
427
+ const p1 = e1.addProperty({ type: 'string' })
428
+
429
+ // Manually add an extra edge to create multiple parents
430
+ domain.graph.setEdge(e2.key, p1.key, { type: 'property' })
431
+
432
+ assert.throws(() => domain.toJSON())
433
+ }).tags(['@modeling', '@serialization', '@validation'])
434
+
435
+ test('should throw validation error when property has non-entity parent', ({ assert }) => {
436
+ const domain = new DataDomain()
437
+ const m1 = domain.addModel()
438
+ const e1 = m1.addEntity()
439
+ const p1 = e1.addProperty({ type: 'string' })
440
+
441
+ // Remove the proper edge and add an edge from model to property (invalid)
442
+ domain.graph.removeEdge(e1.key, p1.key)
443
+ domain.graph.setEdge(m1.key, p1.key, { type: 'property' })
444
+
445
+ assert.throws(() => domain.toJSON())
446
+ }).tags(['@modeling', '@serialization', '@validation'])
447
+
448
+ test('should throw validation error when entity has no model parent', ({ assert }) => {
449
+ const domain = new DataDomain()
450
+ const m1 = domain.addModel()
451
+ const e1 = m1.addEntity()
452
+
453
+ // Remove the entity from its model parent
454
+ domain.graph.setParent(e1.key, undefined)
455
+
456
+ assert.throws(() => domain.toJSON())
457
+ }).tags(['@modeling', '@serialization', '@validation'])
458
+
459
+ test('should throw validation error when association has no target entities', ({ assert }) => {
460
+ const domain = new DataDomain()
461
+ const m1 = domain.addModel()
462
+ const e1 = m1.addEntity()
463
+ const e2 = m1.addEntity()
464
+ const a1 = e1.addAssociation({ key: e2.key })
465
+
466
+ // Remove the association target edge
467
+ domain.graph.removeEdge(a1.key, e2.key)
468
+
469
+ assert.throws(() => domain.toJSON())
470
+ }).tags(['@modeling', '@serialization', '@validation'])
471
+
472
+ test('should throw validation error when association references non-existent target', ({ assert }) => {
473
+ const domain = new DataDomain()
474
+ const m1 = domain.addModel()
475
+ const e1 = m1.addEntity()
476
+ const e2 = m1.addEntity()
477
+ const a1 = e1.addAssociation({ key: e2.key })
478
+
479
+ // Remove the target entity but keep the edge pointing to it
480
+ domain.graph.removeNode(e2.key)
481
+ // Re-add the association with a broken target
482
+ domain.graph.setEdge(a1.key, 'non-existent-entity', { type: 'association' })
483
+
484
+ assert.throws(() => domain.toJSON())
485
+ }).tags(['@modeling', '@serialization', '@validation'])
486
+
487
+ test('should throw validation error when association references non-entity target', ({ assert }) => {
488
+ const domain = new DataDomain()
489
+ const m1 = domain.addModel()
490
+ const e1 = m1.addEntity()
491
+ const e2 = m1.addEntity()
492
+ const a1 = e1.addAssociation({ key: e2.key })
493
+
494
+ // Remove the proper target and make it point to the model instead
495
+ domain.graph.removeEdge(a1.key, e2.key)
496
+ domain.graph.setEdge(a1.key, m1.key, { type: 'association' })
497
+
498
+ assert.throws(() => domain.toJSON())
499
+ }).tags(['@modeling', '@serialization', '@validation'])
500
+
501
+ test('should pass validation for valid domain structure', ({ assert }) => {
502
+ const domain = new DataDomain()
503
+
504
+ // Create a valid structure with all elements
505
+ const ns1 = domain.addNamespace()
506
+ const m1 = ns1.addModel()
507
+ const m2 = domain.addModel() // Root level model (valid)
508
+ const e1 = m1.addEntity()
509
+ const e2 = m2.addEntity()
510
+ e1.addProperty({ type: 'string' })
511
+ e1.addAssociation({ key: e2.key })
512
+
513
+ // This should not throw any validation errors
514
+ assert.doesNotThrow(() => {
515
+ domain.toJSON()
516
+ })
517
+ }).tags(['@modeling', '@serialization', '@validation'])
518
+
519
+ test('should pass validation for models at root level without namespaces', ({ assert }) => {
520
+ const domain = new DataDomain()
521
+
522
+ // Create models directly at domain level (should be valid)
523
+ const m1 = domain.addModel()
524
+ const m2 = domain.addModel()
525
+ const e1 = m1.addEntity()
526
+ const e2 = m2.addEntity()
527
+ e1.addProperty({ type: 'string' })
528
+ e1.addAssociation({ key: e2.key })
529
+
530
+ // This should not throw any validation errors
531
+ assert.doesNotThrow(() => {
532
+ domain.toJSON()
533
+ })
534
+ }).tags(['@modeling', '@serialization', '@validation'])
535
+
536
+ test('should pass validation for foreign associations', ({ assert }) => {
537
+ const fd = new DataDomain()
538
+ const fm1 = fd.addModel()
539
+ const fe1 = fm1.addEntity()
540
+ fd.info.version = '1.0.0'
541
+
542
+ const domain = new DataDomain()
543
+ domain.registerForeignDomain(fd)
544
+ const m1 = domain.addModel()
545
+ const e1 = m1.addEntity()
546
+ e1.addAssociation({ key: fe1.key, domain: fd.key })
547
+
548
+ // Foreign associations should pass validation
549
+ assert.doesNotThrow(() => {
550
+ domain.toJSON()
551
+ })
552
+ }).tags(['@modeling', '@serialization', '@validation'])
553
+ })
554
+
555
+ test.group('DataDomain Lenient Mode Deserialization', () => {
556
+ test('should handle malformed node entries in lenient mode', ({ assert }) => {
557
+ const domain = new DataDomain()
558
+ const m1 = domain.addModel()
559
+ const e1 = m1.addEntity()
560
+ e1.addProperty({ type: 'string' })
561
+
562
+ const serialized = domain.toJSON()
563
+
564
+ // Corrupt a node entry by removing its kind
565
+ const corruptedSerialized = structuredClone(serialized)
566
+ if (corruptedSerialized.graph?.nodes?.[2]) {
567
+ // Find the property node and corrupt it
568
+ for (const node of corruptedSerialized.graph.nodes) {
569
+ if (
570
+ node.value &&
571
+ typeof node.value === 'object' &&
572
+ 'kind' in node.value &&
573
+ node.value.kind === DomainPropertyKind
574
+ ) {
575
+ delete node.value.kind
576
+ break
577
+ }
578
+ }
579
+ }
580
+
581
+ // Should not throw in lenient mode
582
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
583
+
584
+ // Should have issues recorded
585
+ assert.isAbove(restored.issues.length, 0)
586
+ const malformedIssues = restored.issues.filter((issue) => issue.type === 'malformed_entry')
587
+ assert.isAbove(malformedIssues.length, 0)
588
+
589
+ // Should have fewer nodes than original (corrupted node was skipped)
590
+ assert.isBelow([...restored.graph.nodes()].length, [...domain.graph.nodes()].length)
591
+ }).tags(['@modeling', '@serialization', '@lenient'])
592
+
593
+ test('should handle missing parent edges in lenient mode', ({ assert }) => {
594
+ const domain = new DataDomain()
595
+ const m1 = domain.addModel()
596
+ const e1 = m1.addEntity()
597
+ const p1 = e1.addProperty({ type: 'string' })
598
+
599
+ const serialized = domain.toJSON()
600
+
601
+ // Remove the edge from entity to property
602
+ const corruptedSerialized = structuredClone(serialized)
603
+ if (corruptedSerialized.graph?.edges) {
604
+ corruptedSerialized.graph.edges = corruptedSerialized.graph.edges.filter(
605
+ (edge) => !(edge.v === e1.key && edge.w === p1.key)
606
+ )
607
+ }
608
+
609
+ // Should not throw in lenient mode
610
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
611
+
612
+ // Should have issues recorded
613
+ assert.isAbove(restored.issues.length, 0)
614
+ const missingEdgeIssues = restored.issues.filter((issue) => issue.type === 'missing_edge')
615
+ assert.isAbove(missingEdgeIssues.length, 0)
616
+
617
+ // Property should be missing (was skipped due to missing parent edge)
618
+ assert.isUndefined(restored.findProperty(p1.key))
619
+
620
+ // But entity should still exist
621
+ assert.isDefined(restored.findEntity(e1.key))
622
+ }).tags(['@modeling', '@serialization', '@lenient'])
623
+
624
+ test('should handle missing target nodes in lenient mode', ({ assert }) => {
625
+ const domain = new DataDomain()
626
+ const m1 = domain.addModel()
627
+ const e1 = m1.addEntity()
628
+ const e2 = m1.addEntity()
629
+ const a1 = e1.addAssociation({ key: e2.key })
630
+
631
+ const serialized = domain.toJSON()
632
+
633
+ // Remove the target entity node but keep the edge
634
+ const corruptedSerialized = structuredClone(serialized)
635
+ if (corruptedSerialized.graph?.nodes) {
636
+ corruptedSerialized.graph.nodes = corruptedSerialized.graph.nodes.filter((node) => node.v !== e2.key)
637
+ }
638
+
639
+ // Should not throw in lenient mode
640
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
641
+
642
+ // Should have issues recorded
643
+ assert.isAbove(restored.issues.length, 0)
644
+ const missingNodeIssues = restored.issues.filter((issue) => issue.type === 'missing_node')
645
+ assert.isAbove(missingNodeIssues.length, 0)
646
+
647
+ // Source entity and association should exist
648
+ assert.isDefined(restored.findEntity(e1.key))
649
+ assert.isDefined(restored.findAssociation(a1.key))
650
+
651
+ // Target entity should be missing
652
+ assert.isUndefined(restored.findEntity(e2.key))
653
+
654
+ // Association should have no targets
655
+ const restoredAssociation = restored.findAssociation(a1.key)
656
+ assert.equal([...restoredAssociation!.listTargets()].length, 0)
657
+ }).tags(['@modeling', '@serialization', '@lenient'])
658
+
659
+ test('should handle invalid parent relationships in lenient mode', ({ assert }) => {
660
+ const domain = new DataDomain()
661
+ const ns1 = domain.addNamespace()
662
+ const m1 = ns1.addModel()
663
+ const e1 = m1.addEntity()
664
+
665
+ const serialized = domain.toJSON()
666
+
667
+ // Corrupt parent relationship - make entity point to non-existent parent
668
+ const corruptedSerialized = structuredClone(serialized)
669
+ if (corruptedSerialized.graph?.nodes) {
670
+ for (const node of corruptedSerialized.graph.nodes) {
671
+ if (node.v === e1.key && node.parents) {
672
+ node.parents = ['non-existent-parent'] // Invalid - non-existent parent
673
+ break
674
+ }
675
+ }
676
+ }
677
+
678
+ // Should not throw in lenient mode
679
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
680
+
681
+ // Should have issues recorded about missing parent node
682
+ assert.isAbove(restored.issues.length, 0)
683
+ const missingNodeIssues = restored.issues.filter((issue) => issue.type === 'missing_node')
684
+ assert.isAbove(missingNodeIssues.length, 0)
685
+
686
+ // Entity should still exist but may not have proper parent relationship
687
+ assert.isDefined(restored.findEntity(e1.key))
688
+ }).tags(['@modeling', '@serialization', '@lenient'])
689
+
690
+ test('should handle missing foreign dependencies gracefully in lenient mode', ({ assert }) => {
691
+ const fd = new DataDomain()
692
+ const fm1 = fd.addModel()
693
+ const fe1 = fm1.addEntity()
694
+ fd.info.version = '1.0.0'
695
+
696
+ const domain = new DataDomain()
697
+ domain.registerForeignDomain(fd)
698
+ const m1 = domain.addModel()
699
+ const e1 = m1.addEntity()
700
+ e1.addAssociation({ key: fe1.key, domain: fd.key })
701
+
702
+ const serialized = domain.toJSON()
703
+
704
+ // Deserialize without providing the foreign dependency
705
+ const restored = new DataDomain(serialized, [], 'lenient')
706
+
707
+ // Should have issues recorded about missing dependency
708
+ assert.isAbove(restored.issues.length, 0)
709
+ const missingNodeIssues = restored.issues.filter(
710
+ (issue) => issue.type === 'missing_node' && issue.message.includes('foreign')
711
+ )
712
+ assert.isAbove(missingNodeIssues.length, 0)
713
+
714
+ // Local entities should still exist
715
+ assert.isDefined(restored.findEntity(e1.key))
716
+
717
+ // Foreign entity should not exist
718
+ assert.isUndefined(restored.findForeignEntity(fe1.key, fd.key))
719
+ }).tags(['@modeling', '@serialization', '@lenient'])
720
+
721
+ test('should handle unknown node kinds in lenient mode', ({ assert }) => {
722
+ const domain = new DataDomain()
723
+ const m1 = domain.addModel()
724
+
725
+ const serialized = domain.toJSON()
726
+
727
+ // Add a node with unknown kind
728
+ const corruptedSerialized = structuredClone(serialized)
729
+ if (corruptedSerialized.graph?.nodes) {
730
+ corruptedSerialized.graph.nodes.push({
731
+ v: 'unknown-node',
732
+ value: {
733
+ kind: 'UnknownKind',
734
+ key: 'unknown-node',
735
+ info: { name: 'Unknown Node' },
736
+ },
737
+ })
738
+ }
739
+
740
+ // Should not throw in lenient mode
741
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
742
+
743
+ // Should have issues recorded
744
+ assert.isAbove(restored.issues.length, 0)
745
+ const unknownKindIssues = restored.issues.filter((issue) => issue.type === 'unknown_kind')
746
+ assert.isAbove(unknownKindIssues.length, 0)
747
+
748
+ // Original model should still exist
749
+ assert.isDefined(restored.findModel(m1.key))
750
+
751
+ // Unknown node should not exist in graph
752
+ assert.isFalse(restored.graph.hasNode('unknown-node'))
753
+ }).tags(['@modeling', '@serialization', '@lenient'])
754
+
755
+ test('should collect multiple issues and continue processing in lenient mode', ({ assert }) => {
756
+ const domain = new DataDomain()
757
+ const m1 = domain.addModel()
758
+ const e1 = m1.addEntity()
759
+ const e2 = m1.addEntity()
760
+ const p1 = e1.addProperty({ type: 'string' })
761
+ e1.addAssociation({ key: e2.key })
762
+
763
+ const serialized = domain.toJSON()
764
+
765
+ // Introduce multiple types of corruption
766
+ const corruptedSerialized = structuredClone(serialized)
767
+ if (corruptedSerialized.graph?.nodes && corruptedSerialized.graph?.edges) {
768
+ // 1. Remove kind from property node
769
+ for (const node of corruptedSerialized.graph.nodes) {
770
+ if (
771
+ node.value &&
772
+ typeof node.value === 'object' &&
773
+ 'kind' in node.value &&
774
+ node.value.kind === DomainPropertyKind
775
+ ) {
776
+ delete node.value.kind
777
+ break
778
+ }
779
+ }
780
+
781
+ // 2. Remove target entity
782
+ corruptedSerialized.graph.nodes = corruptedSerialized.graph.nodes.filter((node) => node.v !== e2.key)
783
+
784
+ // 3. Add unknown kind node
785
+ corruptedSerialized.graph.nodes.push({
786
+ v: 'unknown-node',
787
+ value: {
788
+ kind: 'UnknownKind',
789
+ key: 'unknown-node',
790
+ info: { name: 'Unknown Node' },
791
+ },
792
+ })
793
+ }
794
+
795
+ // Should not throw in lenient mode
796
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
797
+
798
+ // Should have multiple issues recorded
799
+ assert.isAtLeast(restored.issues.length, 3)
800
+
801
+ // Should have different types of issues
802
+ const issueTypes = new Set(restored.issues.map((issue) => issue.type))
803
+ assert.isTrue(issueTypes.has('malformed_entry') || issueTypes.has('unknown_kind'))
804
+ assert.isTrue(issueTypes.has('missing_node'))
805
+
806
+ // Should still have functional entities where possible
807
+ assert.isDefined(restored.findEntity(e1.key))
808
+ assert.isDefined(restored.findModel(m1.key))
809
+
810
+ // Corrupted elements should be missing
811
+ assert.isUndefined(restored.findProperty(p1.key)) // Property was corrupted
812
+ assert.isUndefined(restored.findEntity(e2.key)) // Entity was removed
813
+ }).tags(['@modeling', '@serialization', '@lenient'])
814
+
815
+ test('should have empty issues array in strict mode on successful deserialization', ({ assert }) => {
816
+ const domain = new DataDomain()
817
+ const m1 = domain.addModel()
818
+ const e1 = m1.addEntity()
819
+ e1.addProperty({ type: 'string' })
820
+
821
+ const serialized = domain.toJSON()
822
+ const restored = new DataDomain(serialized, [], 'strict')
823
+
824
+ // Should have no issues in strict mode for valid data
825
+ assert.equal(restored.issues.length, 0)
826
+ }).tags(['@modeling', '@serialization', '@strict'])
827
+
828
+ test('should throw in strict mode but collect issues in lenient mode for same corrupted data', ({ assert }) => {
829
+ const domain = new DataDomain()
830
+ const m1 = domain.addModel()
831
+ const e1 = m1.addEntity()
832
+ e1.addProperty({ type: 'string' })
833
+
834
+ const serialized = domain.toJSON()
835
+
836
+ // Corrupt the data
837
+ const corruptedSerialized = structuredClone(serialized)
838
+ if (corruptedSerialized.graph?.nodes?.[2]) {
839
+ for (const node of corruptedSerialized.graph.nodes) {
840
+ if (
841
+ node.value &&
842
+ typeof node.value === 'object' &&
843
+ 'kind' in node.value &&
844
+ node.value.kind === DomainPropertyKind
845
+ ) {
846
+ delete node.value.kind
847
+ break
848
+ }
849
+ }
850
+ }
851
+
852
+ // Should throw in strict mode
853
+ assert.throws(() => {
854
+ new DataDomain(corruptedSerialized, [], 'strict')
855
+ })
856
+
857
+ // Should not throw in lenient mode
858
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
859
+ assert.isAbove(restored.issues.length, 0)
860
+ }).tags(['@modeling', '@serialization', '@lenient', '@strict'])
861
+
862
+ test('should provide helpful issue messages and context in lenient mode', ({ assert }) => {
863
+ const domain = new DataDomain()
864
+ const m1 = domain.addModel()
865
+ const e1 = m1.addEntity()
866
+ const p1 = e1.addProperty({ type: 'string', info: { name: 'testProperty' } })
867
+
868
+ const serialized = domain.toJSON()
869
+
870
+ // Remove the edge from entity to property
871
+ const corruptedSerialized = structuredClone(serialized)
872
+ if (corruptedSerialized.graph?.edges) {
873
+ corruptedSerialized.graph.edges = corruptedSerialized.graph.edges.filter(
874
+ (edge) => !(edge.v === e1.key && edge.w === p1.key)
875
+ )
876
+ }
877
+
878
+ const restored = new DataDomain(corruptedSerialized, [], 'lenient')
879
+
880
+ // Should have detailed issue information
881
+ assert.isAbove(restored.issues.length, 0)
882
+ const missingEdgeIssue = restored.issues.find((issue) => issue.type === 'missing_edge')
883
+ assert.isDefined(missingEdgeIssue)
884
+
885
+ // Should have helpful message
886
+ assert.include(missingEdgeIssue!.message, 'testProperty')
887
+ assert.include(missingEdgeIssue!.message.toLowerCase(), 'parent entity edge')
888
+
889
+ // Should have affected key
890
+ assert.equal(missingEdgeIssue!.affectedKey, p1.key)
891
+
892
+ // Should have resolution info
893
+ assert.include(missingEdgeIssue!.resolution!, 'skipped')
894
+
895
+ // Should have appropriate severity
896
+ assert.equal(missingEdgeIssue!.severity, 'error')
897
+ }).tags(['@modeling', '@serialization', '@lenient'])
898
+ })
@@ -36,14 +36,14 @@ test.group('SchemaFilteringStrategy', () => {
36
36
  test('should identify conceptual marker schemas', ({ assert }) => {
37
37
  const strategy = new SchemaFilteringStrategy({ excludeConceptualMarkers: true })
38
38
 
39
- // StatusEnumeration-like schema
39
+ // StatusEnumeration-like schema (empty object, should be excluded)
40
40
  const statusEnum: JSONSchema7 = {
41
41
  type: 'object',
42
42
  title: 'StatusEnumeration',
43
43
  description: 'Lists or enumerations dealing with status types.',
44
44
  }
45
45
 
46
- // ActionStatusType-like schema (allOf with reference only)
46
+ // ActionStatusType-like schema (allOf with reference only - represents inheritance, should NOT be excluded)
47
47
  const actionStatus: JSONSchema7 = {
48
48
  type: 'object',
49
49
  title: 'ActionStatusType',
@@ -61,10 +61,54 @@ test.group('SchemaFilteringStrategy', () => {
61
61
  }
62
62
 
63
63
  assert.isTrue(strategy.shouldExcludeSchema(statusEnum))
64
- assert.isTrue(strategy.shouldExcludeSchema(actionStatus))
64
+ assert.isFalse(strategy.shouldExcludeSchema(actionStatus)) // Changed: inheritance should not be excluded
65
65
  assert.isFalse(strategy.shouldExcludeSchema(person))
66
66
  })
67
67
 
68
+ test('should preserve schema inheritance patterns', ({ assert }) => {
69
+ const strategy = new SchemaFilteringStrategy({ excludeConceptualMarkers: true })
70
+
71
+ // BlogPosting schema - extends SocialMediaPosting via allOf
72
+ const blogPosting: JSONSchema7 = {
73
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
74
+ $id: 'schema:BlogPosting',
75
+ title: 'BlogPosting',
76
+ description: 'A blog post.',
77
+ type: 'object',
78
+ allOf: [
79
+ {
80
+ description: 'A post to a social media platform, including blog posts, tweets, Facebook posts, etc.',
81
+ $ref: 'schema:SocialMediaPosting',
82
+ },
83
+ ],
84
+ }
85
+
86
+ // SocialMediaPosting schema - has properties and extends Article
87
+ const socialMediaPosting: JSONSchema7 = {
88
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
89
+ $id: 'schema:SocialMediaPosting',
90
+ title: 'SocialMediaPosting',
91
+ description: 'A post to a social media platform, including blog posts, tweets, Facebook posts, etc.',
92
+ type: 'object',
93
+ allOf: [
94
+ {
95
+ description: 'An article, such as a news article or piece of investigative report.',
96
+ $ref: 'schema:Article',
97
+ },
98
+ ],
99
+ properties: {
100
+ sharedContent: {
101
+ description: 'A CreativeWork such as an image, video, or audio clip shared as part of this posting.',
102
+ oneOf: [{ $ref: 'schema:CreativeWork' }, { type: 'array', items: { $ref: 'schema:CreativeWork' } }],
103
+ },
104
+ },
105
+ }
106
+
107
+ // Both schemas represent valid inheritance and should be included
108
+ assert.isFalse(strategy.shouldExcludeSchema(blogPosting, 'schema:BlogPosting'))
109
+ assert.isFalse(strategy.shouldExcludeSchema(socialMediaPosting, 'schema:SocialMediaPosting'))
110
+ })
111
+
68
112
  test('should filter by patterns', ({ assert }) => {
69
113
  const strategy = new SchemaFilteringStrategy({
70
114
  excludePatterns: [/.*Enumeration$/, /.*Type$/],