@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.
@@ -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
+ })