@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.
- package/build/src/modeling/DataDomain.d.ts +7 -2
- package/build/src/modeling/DataDomain.d.ts.map +1 -1
- package/build/src/modeling/DataDomain.js +15 -2
- package/build/src/modeling/DataDomain.js.map +1 -1
- package/build/src/modeling/DomainSerialization.d.ts +6 -3
- package/build/src/modeling/DomainSerialization.d.ts.map +1 -1
- package/build/src/modeling/DomainSerialization.js +374 -52
- package/build/src/modeling/DomainSerialization.js.map +1 -1
- package/build/src/modeling/importers/SchemaFilteringStrategy.d.ts.map +1 -1
- package/build/src/modeling/importers/SchemaFilteringStrategy.js +3 -9
- package/build/src/modeling/importers/SchemaFilteringStrategy.js.map +1 -1
- package/build/src/modeling/types.d.ts +69 -2
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +15 -15
- package/package.json +1 -1
- package/src/modeling/DataDomain.ts +24 -3
- package/src/modeling/DomainSerialization.ts +442 -56
- package/src/modeling/importers/SchemaFilteringStrategy.ts +3 -11
- package/src/modeling/types.ts +73 -2
- package/tests/unit/modeling/data_domain_serialization.spec.ts +504 -0
- package/tests/unit/modeling/importers/schema_filtering.spec.ts +47 -3
package/src/modeling/types.ts
CHANGED
|
@@ -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
|
|
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 "
|
|
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.
|
|
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$/],
|