@api-client/core 0.18.11 → 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.
@@ -42071,10 +42071,10 @@
42071
42071
  "@id": "#197"
42072
42072
  },
42073
42073
  {
42074
- "@id": "#203"
42074
+ "@id": "#200"
42075
42075
  },
42076
42076
  {
42077
- "@id": "#200"
42077
+ "@id": "#203"
42078
42078
  },
42079
42079
  {
42080
42080
  "@id": "#206"
@@ -42810,16 +42810,16 @@
42810
42810
  "@id": "#219"
42811
42811
  },
42812
42812
  {
42813
- "@id": "#216"
42813
+ "@id": "#210"
42814
42814
  },
42815
42815
  {
42816
- "@id": "#219"
42816
+ "@id": "#213"
42817
42817
  },
42818
42818
  {
42819
- "@id": "#210"
42819
+ "@id": "#216"
42820
42820
  },
42821
42821
  {
42822
- "@id": "#213"
42822
+ "@id": "#219"
42823
42823
  }
42824
42824
  ],
42825
42825
  "doc:root": false,
@@ -43499,7 +43499,7 @@
43499
43499
  "doc:ExternalDomainElement",
43500
43500
  "doc:DomainElement"
43501
43501
  ],
43502
- "doc:raw": "code: 'J'\ndescription: 'Information and communication'\n",
43502
+ "doc:raw": "class: '3'\ndescription: '150 - 300'\nnumberOfFte: 5500\nnumberOfEmployees: 5232\n",
43503
43503
  "core:mediaType": "application/yaml",
43504
43504
  "sourcemaps:sources": [
43505
43505
  {
@@ -43520,7 +43520,7 @@
43520
43520
  "doc:ExternalDomainElement",
43521
43521
  "doc:DomainElement"
43522
43522
  ],
43523
- "doc:raw": "class: '3'\ndescription: '150 - 300'\nnumberOfFte: 5500\nnumberOfEmployees: 5232\n",
43523
+ "doc:raw": "code: 'J'\ndescription: 'Information and communication'\n",
43524
43524
  "core:mediaType": "application/yaml",
43525
43525
  "sourcemaps:sources": [
43526
43526
  {
@@ -44232,7 +44232,7 @@
44232
44232
  "doc:ExternalDomainElement",
44233
44233
  "doc:DomainElement"
44234
44234
  ],
44235
- "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44235
+ "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '22'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)22 000000'\n",
44236
44236
  "core:mediaType": "application/yaml",
44237
44237
  "sourcemaps:sources": [
44238
44238
  {
@@ -44253,7 +44253,7 @@
44253
44253
  "doc:ExternalDomainElement",
44254
44254
  "doc:DomainElement"
44255
44255
  ],
44256
- "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44256
+ "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
44257
44257
  "core:mediaType": "application/yaml",
44258
44258
  "sourcemaps:sources": [
44259
44259
  {
@@ -44274,7 +44274,7 @@
44274
44274
  "doc:ExternalDomainElement",
44275
44275
  "doc:DomainElement"
44276
44276
  ],
44277
- "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '22'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)22 000000'\n",
44277
+ "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44278
44278
  "core:mediaType": "application/yaml",
44279
44279
  "sourcemaps:sources": [
44280
44280
  {
@@ -44295,7 +44295,7 @@
44295
44295
  "doc:ExternalDomainElement",
44296
44296
  "doc:DomainElement"
44297
44297
  ],
44298
- "doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
44298
+ "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44299
44299
  "core:mediaType": "application/yaml",
44300
44300
  "sourcemaps:sources": [
44301
44301
  {
@@ -44771,12 +44771,12 @@
44771
44771
  {
44772
44772
  "@id": "#202/source-map/lexical/element_0",
44773
44773
  "sourcemaps:element": "amf://id#202",
44774
- "sourcemaps:value": "[(1,0)-(3,0)]"
44774
+ "sourcemaps:value": "[(1,0)-(5,0)]"
44775
44775
  },
44776
44776
  {
44777
44777
  "@id": "#205/source-map/lexical/element_0",
44778
44778
  "sourcemaps:element": "amf://id#205",
44779
- "sourcemaps:value": "[(1,0)-(5,0)]"
44779
+ "sourcemaps:value": "[(1,0)-(3,0)]"
44780
44780
  },
44781
44781
  {
44782
44782
  "@id": "#208/source-map/lexical/element_0",
@@ -45116,22 +45116,22 @@
45116
45116
  {
45117
45117
  "@id": "#212/source-map/lexical/element_0",
45118
45118
  "sourcemaps:element": "amf://id#212",
45119
- "sourcemaps:value": "[(1,0)-(7,0)]"
45119
+ "sourcemaps:value": "[(1,0)-(6,0)]"
45120
45120
  },
45121
45121
  {
45122
45122
  "@id": "#215/source-map/lexical/element_0",
45123
45123
  "sourcemaps:element": "amf://id#215",
45124
- "sourcemaps:value": "[(1,0)-(3,0)]"
45124
+ "sourcemaps:value": "[(1,0)-(6,0)]"
45125
45125
  },
45126
45126
  {
45127
45127
  "@id": "#218/source-map/lexical/element_0",
45128
45128
  "sourcemaps:element": "amf://id#218",
45129
- "sourcemaps:value": "[(1,0)-(6,0)]"
45129
+ "sourcemaps:value": "[(1,0)-(7,0)]"
45130
45130
  },
45131
45131
  {
45132
45132
  "@id": "#221/source-map/lexical/element_0",
45133
45133
  "sourcemaps:element": "amf://id#221",
45134
- "sourcemaps:value": "[(1,0)-(6,0)]"
45134
+ "sourcemaps:value": "[(1,0)-(3,0)]"
45135
45135
  },
45136
45136
  {
45137
45137
  "@id": "#338/source-map/synthesized-field/element_1",
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.11",
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
  }
@@ -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
+ })