@api-client/core 0.18.10 → 0.18.12

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.18.10",
4
+ "version": "0.18.12",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -271,11 +271,18 @@ export class DomainModel extends DomainElement {
271
271
  throw new AttachException(`Trying to attach the ${key} entity, but it is already a child of this model`)
272
272
  }
273
273
  parent.detachEntity(key)
274
- this.fields.push({
275
- type: 'entity',
276
- key: key,
277
- })
274
+ if (!this.fields.some((item) => item.key === key)) {
275
+ this.fields.push({
276
+ type: 'entity',
277
+ key: key,
278
+ })
279
+ }
278
280
  this.root.graph.setParent(key, this.key)
281
+ // verify that the entity is moved to the current model
282
+ const graphParent = this.root.graph.parent(key)
283
+ if (graphParent !== this.key) {
284
+ throw new AttachException(`Trying to attach the ${key} entity, but it is not a child of this model`)
285
+ }
279
286
  this.root.notifyChange()
280
287
  return this
281
288
  }
@@ -272,19 +272,25 @@ export function deserialize(root: DataDomain, json?: SerializedGraph, dependenci
272
272
  return g
273
273
  }
274
274
  if (Array.isArray(json.nodes)) {
275
+ // 1st pass - set up nodes
276
+ const parentInfo = new Map<string, string[]>()
275
277
  for (const entry of json.nodes) {
276
278
  g.setNode(entry.v, prepareNode(root, entry.value, json.edges))
277
279
  if (entry.parents) {
278
- for (const parent of entry.parents) {
279
- // In data domain graph, all nodes that can have parents can only have a single parent.
280
- // It's the business logic of the library.
281
- // Parent-child relationships:
282
- // - Namespace -> Namespace
283
- // - Namespace -> Model
284
- // - Model -> Entity
285
- // Entities and Association are associated with the parent entity through edges.
286
- g.setParent(entry.v, parent)
287
- }
280
+ parentInfo.set(entry.v, entry.parents)
281
+ }
282
+ }
283
+ // 2nd pass - set up parents
284
+ 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.
292
+ for (const parent of parents) {
293
+ g.setParent(key, parent)
288
294
  }
289
295
  }
290
296
  }
File without changes
@@ -291,4 +291,104 @@ test.group('DataDomain Serialization and Deserialization', () => {
291
291
  assert.isTrue(restored.graph.hasEdge(e1.key, a1.key), 'has edge from e1 to a1')
292
292
  assert.isFalse(restored.graph.hasEdge(a1.key, `${fd.key}:${fe1.key}`), 'has no edge from a1 to fe1')
293
293
  }).tags(['@modeling', '@serialization'])
294
+
295
+ test('should serialize and deserialize parent-child relationships in the graph', ({ assert }) => {
296
+ const domain = new DataDomain()
297
+
298
+ // Create hierarchical structure: root > ns1 > ns2 > m1 > e1, e2 (e2 inherits from e1)
299
+ const ns1 = domain.addNamespace({ key: 'ns1' })
300
+ const ns2 = ns1.addNamespace({ key: 'ns2' })
301
+ const m1 = ns2.addModel({ key: 'm1' })
302
+ const m2 = domain.addModel({ key: 'm2' }) // root level model
303
+ const e1 = m1.addEntity({ key: 'e1' })
304
+ const e2 = m1.addEntity({ key: 'e2' })
305
+ const e3 = m2.addEntity({ key: 'e3' })
306
+
307
+ // Add entity inheritance relationships
308
+ e2.addParent(e1.key) // e2 inherits from e1
309
+ e3.addParent(e2.key) // e3 inherits from e2 (multi-level inheritance)
310
+
311
+ // Add properties and associations
312
+ const p1 = e1.addProperty({ type: 'string', key: 'p1' })
313
+ const p2 = e2.addProperty({ type: 'number', key: 'p2' })
314
+ const a1 = e1.addAssociation({ key: e3.key }, { key: 'a1' })
315
+
316
+ const serialized = domain.toJSON()
317
+ const restored = new DataDomain(serialized)
318
+
319
+ // Verify basic domain properties
320
+ assert.equal(restored.key, domain.key)
321
+ assert.equal(restored.kind, domain.kind)
322
+ assert.deepEqual(restored.info.toJSON(), domain.info.toJSON())
323
+
324
+ // Verify all nodes are restored
325
+ assert.equal([...restored.graph.nodes()].length, 10) // 2 ns + 2 models + 3 entities + 2 properties + 1 association
326
+
327
+ // Verify namespace hierarchy: ns1 (root) -> ns2 (child of ns1)
328
+ const rns1 = restored.findNamespace(ns1.key)
329
+ const rns2 = restored.findNamespace(ns2.key)
330
+ assert.isDefined(rns1)
331
+ assert.isDefined(rns2)
332
+ assert.isUndefined(restored.graph.parent(ns1.key), 'ns1 should be at root level')
333
+ assert.equal(restored.graph.parent(ns2.key), ns1.key, 'ns2 should be child of ns1')
334
+
335
+ // Verify model hierarchy: m1 (child of ns2), m2 (root level)
336
+ const rm1 = restored.findModel(m1.key)
337
+ const rm2 = restored.findModel(m2.key)
338
+ assert.isDefined(rm1)
339
+ assert.isDefined(rm2)
340
+ assert.equal(restored.graph.parent(m1.key), ns2.key, 'm1 should be child of ns2')
341
+ assert.isUndefined(restored.graph.parent(m2.key), 'm2 should be at root level')
342
+
343
+ // Verify entity hierarchy: e1, e2 (children of m1), e3 (child of m2)
344
+ const re1 = restored.findEntity(e1.key)
345
+ const re2 = restored.findEntity(e2.key)
346
+ const re3 = restored.findEntity(e3.key)
347
+ assert.isDefined(re1)
348
+ assert.isDefined(re2)
349
+ assert.isDefined(re3)
350
+ assert.equal(restored.graph.parent(e1.key), m1.key, 'e1 should be child of m1')
351
+ assert.equal(restored.graph.parent(e2.key), m1.key, 'e2 should be child of m1')
352
+ assert.equal(restored.graph.parent(e3.key), m2.key, 'e3 should be child of m2')
353
+
354
+ // Verify entity inheritance relationships (via edges, not parent-child)
355
+ assert.isTrue(restored.graph.hasEdge(e2.key, e1.key), 'e2 should inherit from e1')
356
+ assert.equal(restored.graph.edge(e2.key, e1.key)?.type, 'parent', 'inheritance edge should have type parent')
357
+ assert.isTrue(restored.graph.hasEdge(e3.key, e2.key), 'e3 should inherit from e2')
358
+ assert.equal(restored.graph.edge(e3.key, e2.key)?.type, 'parent', 'inheritance edge should have type parent')
359
+
360
+ // Verify entity inheritance works functionally
361
+ const restoredParentsE2 = [...re2!.listParents()]
362
+ const restoredParentsE3 = [...re3!.listParents()]
363
+ assert.equal(restoredParentsE2.length, 1, 'e2 should have 1 parent')
364
+ assert.equal(restoredParentsE2[0].key, e1.key, 'e2 parent should be e1')
365
+ assert.equal(restoredParentsE3.length, 1, 'e3 should have 1 parent')
366
+ assert.equal(restoredParentsE3[0].key, e2.key, 'e3 parent should be e2')
367
+
368
+ // Verify property hierarchy: p1 (child of e1), p2 (child of e2)
369
+ const rp1 = restored.findProperty(p1.key)
370
+ const rp2 = restored.findProperty(p2.key)
371
+ assert.isDefined(rp1)
372
+ assert.isDefined(rp2)
373
+ assert.isTrue(restored.graph.hasEdge(e1.key, p1.key), 'e1 should have edge to p1')
374
+ assert.equal(restored.graph.edge(e1.key, p1.key)?.type, 'property', 'property edge should have type property')
375
+ assert.isTrue(restored.graph.hasEdge(e2.key, p2.key), 'e2 should have edge to p2')
376
+ assert.equal(restored.graph.edge(e2.key, p2.key)?.type, 'property', 'property edge should have type property')
377
+
378
+ // Verify association hierarchy: a1 (child of e1) -> e3
379
+ const ra1 = restored.findAssociation(a1.key)
380
+ assert.isDefined(ra1)
381
+ assert.isTrue(restored.graph.hasEdge(e1.key, a1.key), 'e1 should have edge to a1')
382
+ assert.equal(
383
+ restored.graph.edge(e1.key, a1.key)?.type,
384
+ 'association',
385
+ 'association edge should have type association'
386
+ )
387
+ assert.isTrue(restored.graph.hasEdge(a1.key, e3.key), 'a1 should have edge to e3')
388
+ assert.equal(
389
+ restored.graph.edge(a1.key, e3.key)?.type,
390
+ 'association',
391
+ 'association target edge should have type association'
392
+ )
393
+ }).tags(['@modeling', '@serialization'])
294
394
  })
@@ -1,5 +1,5 @@
1
1
  import { test } from '@japa/runner'
2
- import { DataDomain, DomainEntity, DomainEntityKind } from '../../../src/index.js'
2
+ import { DataDomain, DomainEntity, DomainEntityKind, DomainModel } from '../../../src/index.js'
3
3
 
4
4
  test.group('DomainModel.addEntity()', () => {
5
5
  test('adds an entity to the model', ({ assert }) => {
@@ -406,3 +406,308 @@ test.group('DomainModel.hasEntities()', () => {
406
406
  assert.isFalse(model.hasEntities())
407
407
  })
408
408
  })
409
+
410
+ test.group('DomainModel.attachEntity() - Race Condition Tests', () => {
411
+ test('simulate multiple entities moved to same target simultaneously', ({ assert }) => {
412
+ const dataDomain = new DataDomain()
413
+ const sourceModel1 = dataDomain.addModel({ key: 'source1' })
414
+ const sourceModel2 = dataDomain.addModel({ key: 'source2' })
415
+ const targetModel = dataDomain.addModel({ key: 'target' })
416
+
417
+ const entity1 = sourceModel1.addEntity({ key: 'entity1' })
418
+ const entity2 = sourceModel2.addEntity({ key: 'entity2' })
419
+
420
+ // Move multiple entities to the same target
421
+ targetModel.attachEntity(entity1.key)
422
+ targetModel.attachEntity(entity2.key)
423
+
424
+ // Verify no duplication in fields array
425
+ const entityFields = targetModel.fields.filter((f) => f.type === 'entity')
426
+ const entityKeys = entityFields.map((f) => f.key)
427
+ const uniqueKeys = [...new Set(entityKeys)]
428
+ assert.equal(entityFields.length, uniqueKeys.length, 'No duplicate entities in fields array')
429
+ assert.lengthOf(entityFields, 2, 'Should have exactly 2 entities')
430
+ assert.include(entityKeys, entity1.key, 'Should contain entity1')
431
+ assert.include(entityKeys, entity2.key, 'Should contain entity2')
432
+
433
+ // Verify graph consistency
434
+ assert.equal(dataDomain.graph.parent(entity1.key), targetModel.key, 'Entity1 parent should be target model')
435
+ assert.equal(dataDomain.graph.parent(entity2.key), targetModel.key, 'Entity2 parent should be target model')
436
+
437
+ // Verify entities removed from source models
438
+ assert.lengthOf(sourceModel1.fields, 0, 'Source1 should have no entities')
439
+ assert.lengthOf(sourceModel2.fields, 0, 'Source2 should have no entities')
440
+ })
441
+
442
+ test('simulate entity moved between models multiple times rapidly', ({ assert }) => {
443
+ const dataDomain = new DataDomain()
444
+ const model1 = dataDomain.addModel({ key: 'model1' })
445
+ const model2 = dataDomain.addModel({ key: 'model2' })
446
+ const model3 = dataDomain.addModel({ key: 'model3' })
447
+
448
+ const entity = model1.addEntity({ key: 'entity' })
449
+
450
+ // Simulate rapid moves
451
+ model2.attachEntity(entity.key)
452
+ model3.attachEntity(entity.key)
453
+ model1.attachEntity(entity.key)
454
+ model2.attachEntity(entity.key)
455
+
456
+ // Final state verification
457
+ assert.lengthOf(model2.fields, 1, 'Model2 should have 1 entity')
458
+ assert.lengthOf(model1.fields, 0, 'Model1 should have no entities')
459
+ assert.lengthOf(model3.fields, 0, 'Model3 should have no entities')
460
+ assert.equal(dataDomain.graph.parent(entity.key), model2.key, 'Entity parent should be model2')
461
+ })
462
+
463
+ test('simulate concurrent attachment of same entity to different targets', ({ assert }) => {
464
+ const dataDomain = new DataDomain()
465
+ const sourceModel = dataDomain.addModel({ key: 'source' })
466
+ const targetModel1 = dataDomain.addModel({ key: 'target1' })
467
+ const targetModel2 = dataDomain.addModel({ key: 'target2' })
468
+
469
+ const entity = sourceModel.addEntity({ key: 'entity' })
470
+
471
+ // First move should succeed
472
+ targetModel1.attachEntity(entity.key)
473
+ assert.equal(dataDomain.graph.parent(entity.key), targetModel1.key, 'Entity should be in target1')
474
+ assert.lengthOf(targetModel1.fields, 1, 'Target1 should have the entity')
475
+ assert.lengthOf(sourceModel.fields, 0, 'Source should be empty')
476
+
477
+ // Second move should move from target1 to target2
478
+ targetModel2.attachEntity(entity.key)
479
+ assert.equal(dataDomain.graph.parent(entity.key), targetModel2.key, 'Entity should be in target2')
480
+ assert.lengthOf(targetModel2.fields, 1, 'Target2 should have the entity')
481
+ assert.lengthOf(targetModel1.fields, 0, 'Target1 should be empty')
482
+ assert.lengthOf(sourceModel.fields, 0, 'Source should still be empty')
483
+ })
484
+
485
+ test('verify entity is not duplicated when moved from model with multiple entities', ({ assert }) => {
486
+ const dataDomain = new DataDomain()
487
+ const sourceModel = dataDomain.addModel({ key: 'source' })
488
+ const targetModel = dataDomain.addModel({ key: 'target' })
489
+
490
+ const entity1 = sourceModel.addEntity({ key: 'entity1' })
491
+ const entity2 = sourceModel.addEntity({ key: 'entity2' })
492
+ const entity3 = sourceModel.addEntity({ key: 'entity3' })
493
+
494
+ // Move middle entity
495
+ targetModel.attachEntity(entity2.key)
496
+
497
+ // Verify source model has 2 entities remaining
498
+ assert.lengthOf(sourceModel.fields, 2, 'Source should have 2 entities remaining')
499
+ const sourceKeys = sourceModel.fields.map((f) => f.key)
500
+ assert.include(sourceKeys, entity1.key, 'Should contain entity1')
501
+ assert.include(sourceKeys, entity3.key, 'Should contain entity3')
502
+ assert.notInclude(sourceKeys, entity2.key, 'Should not contain moved entity2')
503
+
504
+ // Verify target model has 1 entity
505
+ assert.lengthOf(targetModel.fields, 1, 'Target should have 1 entity')
506
+ assert.equal(targetModel.fields[0].key, entity2.key, 'Target should have entity2')
507
+
508
+ // Verify graph consistency
509
+ assert.equal(dataDomain.graph.parent(entity1.key), sourceModel.key, 'Entity1 parent should be source')
510
+ assert.equal(dataDomain.graph.parent(entity2.key), targetModel.key, 'Entity2 parent should be target')
511
+ assert.equal(dataDomain.graph.parent(entity3.key), sourceModel.key, 'Entity3 parent should be source')
512
+ })
513
+
514
+ test('verify field consistency when moving entities with properties', ({ assert }) => {
515
+ const dataDomain = new DataDomain()
516
+ const sourceModel = dataDomain.addModel({ key: 'source' })
517
+ const targetModel = dataDomain.addModel({ key: 'target' })
518
+
519
+ const entity = sourceModel.addEntity({ key: 'entity' })
520
+ entity.addProperty({ key: 'prop1', type: 'string' })
521
+ entity.addProperty({ key: 'prop2', type: 'number' })
522
+
523
+ // Verify initial state
524
+ assert.lengthOf(entity.fields, 2, 'Entity should have 2 properties')
525
+
526
+ // Move entity
527
+ targetModel.attachEntity(entity.key)
528
+
529
+ // Verify entity fields are preserved
530
+ assert.lengthOf(entity.fields, 2, 'Entity should still have 2 properties after move')
531
+ assert.equal(dataDomain.graph.parent(entity.key), targetModel.key, 'Entity parent should be target')
532
+
533
+ // Verify model fields
534
+ assert.lengthOf(sourceModel.fields, 0, 'Source model should be empty')
535
+ assert.lengthOf(targetModel.fields, 1, 'Target model should have 1 entity')
536
+ })
537
+
538
+ test('stress test - multiple entities moved in sequence to verify no field array corruption', ({ assert }) => {
539
+ const dataDomain = new DataDomain()
540
+ const sourceModels = []
541
+ const targetModel = dataDomain.addModel({ key: 'target' })
542
+ const entities = []
543
+
544
+ // Create 10 source models with 1 entity each
545
+ for (let i = 0; i < 10; i++) {
546
+ const sourceModel = dataDomain.addModel({ key: `source${i}` })
547
+ sourceModels.push(sourceModel)
548
+ const entity = sourceModel.addEntity({ key: `entity${i}` })
549
+ entities.push(entity)
550
+ }
551
+
552
+ // Move all entities to target model
553
+ for (const entity of entities) {
554
+ targetModel.attachEntity(entity.key)
555
+ }
556
+
557
+ // Verify target model state
558
+ assert.lengthOf(targetModel.fields, 10, 'Target should have 10 entities')
559
+ const targetKeys = targetModel.fields.map((f) => f.key).sort()
560
+ const expectedKeys = entities.map((e) => e.key).sort()
561
+ assert.deepEqual(targetKeys, expectedKeys, 'Target should have all expected entities')
562
+
563
+ // Verify no duplicates
564
+ const uniqueKeys = [...new Set(targetKeys)]
565
+ assert.equal(targetKeys.length, uniqueKeys.length, 'No duplicate keys in target')
566
+
567
+ // Verify source models are empty
568
+ for (const sourceModel of sourceModels) {
569
+ assert.lengthOf(sourceModel.fields, 0, `Source model ${sourceModel.key} should be empty`)
570
+ }
571
+
572
+ // Verify graph consistency
573
+ for (const entity of entities) {
574
+ assert.equal(dataDomain.graph.parent(entity.key), targetModel.key, `Entity ${entity.key} parent should be target`)
575
+ }
576
+ })
577
+
578
+ test('verify detach-attach sequence atomicity', ({ assert }) => {
579
+ const dataDomain = new DataDomain()
580
+ const model1 = dataDomain.addModel({ key: 'model1' })
581
+ const model2 = dataDomain.addModel({ key: 'model2' })
582
+
583
+ const entity = model1.addEntity({ key: 'entity' })
584
+
585
+ // Spy on the detachEntity method to verify it's called
586
+ let detachCalled = false
587
+ const originalDetach = model1.detachEntity
588
+ model1.detachEntity = function (key: string) {
589
+ detachCalled = true
590
+ return originalDetach.call(this, key)
591
+ }
592
+
593
+ model2.attachEntity(entity.key)
594
+
595
+ // Verify detach was called during attach
596
+ assert.isTrue(detachCalled, 'detachEntity should have been called on the source model')
597
+
598
+ // Verify final state
599
+ assert.lengthOf(model1.fields, 0, 'Model1 should be empty')
600
+ assert.lengthOf(model2.fields, 1, 'Model2 should have the entity')
601
+ assert.equal(dataDomain.graph.parent(entity.key), model2.key, 'Graph parent should be model2')
602
+ })
603
+
604
+ test('verify graph parent is updated before fields array check', ({ assert }) => {
605
+ const dataDomain = new DataDomain()
606
+ const model1 = dataDomain.addModel({ key: 'model1' })
607
+ const model2 = dataDomain.addModel({ key: 'model2' })
608
+
609
+ const entity = model1.addEntity({ key: 'entity' })
610
+
611
+ // Override setParent to track when it's called
612
+ const parentSetCalls: { nodeKey: string; parentKey?: string; timestamp: number }[] = []
613
+ const originalSetParent = dataDomain.graph.setParent
614
+ dataDomain.graph.setParent = function (nodeKey: string, parentKey?: string) {
615
+ parentSetCalls.push({ nodeKey, parentKey, timestamp: Date.now() })
616
+ return originalSetParent.call(this, nodeKey, parentKey)
617
+ }
618
+
619
+ // Override fields.push to track when it's called
620
+ const fieldsPushCalls: { items: unknown[]; timestamp: number }[] = []
621
+ const originalPush = model2.fields.push
622
+ model2.fields.push = function (...items) {
623
+ fieldsPushCalls.push({ items: [...items], timestamp: Date.now() })
624
+ return originalPush.call(this, ...items)
625
+ }
626
+
627
+ model2.attachEntity(entity.key)
628
+
629
+ // Verify the sequence - setParent should be called twice (detach + attach)
630
+ assert.lengthOf(parentSetCalls, 2, 'setParent should be called twice (detach + attach)')
631
+ assert.lengthOf(fieldsPushCalls, 1, 'fields.push should be called once')
632
+
633
+ // First call should clear the parent (detach)
634
+ assert.equal(parentSetCalls[0].nodeKey, entity.key, 'First call should be for entity key')
635
+ assert.isUndefined(parentSetCalls[0].parentKey, 'First call should clear parent')
636
+
637
+ // Second call should set the new parent (attach)
638
+ assert.equal(parentSetCalls[1].parentKey, model2.key, 'Second call should set parent to model2')
639
+ assert.equal(parentSetCalls[1].nodeKey, entity.key, 'Second call should be for entity key')
640
+ })
641
+
642
+ test('detect inconsistent graph parent - entity in fields but wrong graph parent', ({ assert }) => {
643
+ const dataDomain = new DataDomain()
644
+ const sourceModel = dataDomain.addModel({ key: 'source' })
645
+ const targetModel = dataDomain.addModel({ key: 'target' })
646
+
647
+ const entity = sourceModel.addEntity({ key: 'entity' })
648
+
649
+ // Simulate race condition where graph parent is not updated during attach
650
+ const originalSetParent = dataDomain.graph.setParent
651
+ let setParentCallCount = 0
652
+ dataDomain.graph.setParent = function (nodeKey: string, parentKey?: string) {
653
+ setParentCallCount++
654
+ // Skip the second setParent call (the attach operation)
655
+ if (setParentCallCount === 2) {
656
+ return this // Don't actually set the parent
657
+ }
658
+ return originalSetParent.call(this, nodeKey, parentKey)
659
+ }
660
+
661
+ assert.throws(() => {
662
+ targetModel.attachEntity(entity.key)
663
+ })
664
+
665
+ // Restore original method
666
+ dataDomain.graph.setParent = originalSetParent
667
+ })
668
+
669
+ test('verify consistency during rapid sequential moves', ({ assert }) => {
670
+ const dataDomain = new DataDomain()
671
+ const models: DomainModel[] = []
672
+
673
+ // Create multiple models
674
+ for (let i = 0; i < 5; i++) {
675
+ models.push(dataDomain.addModel({ key: `model${i}` }))
676
+ }
677
+
678
+ const entity = models[0].addEntity({ key: 'entity' })
679
+
680
+ // Perform rapid moves between models
681
+ for (let iteration = 0; iteration < 20; iteration++) {
682
+ const targetIndex = (iteration + 1) % models.length
683
+ models[targetIndex].attachEntity(entity.key)
684
+
685
+ // Verify consistency after each move
686
+ let entityCount = 0
687
+ let modelWithEntity = null
688
+
689
+ for (const model of models) {
690
+ const hasEntity = model.fields.some((f) => f.type === 'entity' && f.key === entity.key)
691
+ if (hasEntity) {
692
+ entityCount++
693
+ modelWithEntity = model
694
+ }
695
+ }
696
+
697
+ // Entity should exist in exactly one model
698
+ assert.equal(
699
+ entityCount,
700
+ 1,
701
+ `Iteration ${iteration}: Entity should exist in exactly one model, found in ${entityCount} models`
702
+ )
703
+
704
+ // Graph parent should match the model that has the entity
705
+ const graphParent = dataDomain.graph.parent(entity.key)
706
+ assert.equal(
707
+ graphParent,
708
+ modelWithEntity?.key,
709
+ `Iteration ${iteration}: Graph parent should match model with entity`
710
+ )
711
+ }
712
+ })
713
+ })