@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/build/src/modeling/DomainModel.d.ts.map +1 -1
- package/build/src/modeling/DomainModel.js +11 -4
- package/build/src/modeling/DomainModel.js.map +1 -1
- package/build/src/modeling/DomainSerialization.d.ts.map +1 -1
- package/build/src/modeling/DomainSerialization.js +16 -10
- package/build/src/modeling/DomainSerialization.js.map +1 -1
- package/build/src/runtime/http-engine/CoreEngine2.d.ts +2 -0
- package/build/src/runtime/http-engine/CoreEngine2.d.ts.map +1 -0
- package/build/src/runtime/http-engine/CoreEngine2.js +2 -0
- package/build/src/runtime/http-engine/CoreEngine2.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/modeling/DomainModel.ts +11 -4
- package/src/modeling/DomainSerialization.ts +16 -10
- package/src/runtime/http-engine/CoreEngine2.ts +0 -0
- package/tests/unit/modeling/data_domain_serialization.spec.ts +100 -0
- package/tests/unit/modeling/domain_model_entities.spec.ts +306 -1
package/package.json
CHANGED
|
@@ -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.
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
})
|