@expo/entity 0.43.0 → 0.45.0

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.
Files changed (80) hide show
  1. package/build/AuthorizationResultBasedEntityLoader.js +5 -1
  2. package/build/AuthorizationResultBasedEntityLoader.js.map +1 -1
  3. package/build/AuthorizationResultBasedEntityMutator.d.ts +87 -2
  4. package/build/AuthorizationResultBasedEntityMutator.js +122 -8
  5. package/build/AuthorizationResultBasedEntityMutator.js.map +1 -1
  6. package/build/EntityLoaderUtils.d.ts +15 -3
  7. package/build/EntityLoaderUtils.js +25 -10
  8. package/build/EntityLoaderUtils.js.map +1 -1
  9. package/build/EntityQueryContext.d.ts +53 -7
  10. package/build/EntityQueryContext.js +65 -10
  11. package/build/EntityQueryContext.js.map +1 -1
  12. package/build/EntityQueryContextProvider.d.ts +5 -1
  13. package/build/EntityQueryContextProvider.js +11 -4
  14. package/build/EntityQueryContextProvider.js.map +1 -1
  15. package/build/IEntityGenericCacher.d.ts +2 -2
  16. package/build/errors/EntityNotFoundError.d.ts +8 -1
  17. package/build/errors/EntityNotFoundError.js +7 -2
  18. package/build/errors/EntityNotFoundError.js.map +1 -1
  19. package/build/index.d.ts +1 -0
  20. package/build/index.js +1 -0
  21. package/build/index.js.map +1 -1
  22. package/build/internal/CompositeFieldHolder.d.ts +13 -0
  23. package/build/internal/CompositeFieldHolder.js +7 -0
  24. package/build/internal/CompositeFieldHolder.js.map +1 -1
  25. package/build/internal/CompositeFieldValueMap.d.ts +3 -0
  26. package/build/internal/CompositeFieldValueMap.js +3 -0
  27. package/build/internal/CompositeFieldValueMap.js.map +1 -1
  28. package/build/internal/EntityDataManager.d.ts +22 -3
  29. package/build/internal/EntityDataManager.js +99 -11
  30. package/build/internal/EntityDataManager.js.map +1 -1
  31. package/build/internal/EntityFieldTransformationUtils.d.ts +20 -0
  32. package/build/internal/EntityFieldTransformationUtils.js +15 -0
  33. package/build/internal/EntityFieldTransformationUtils.js.map +1 -1
  34. package/build/internal/EntityLoadInterfaces.d.ts +8 -0
  35. package/build/internal/EntityLoadInterfaces.js +2 -0
  36. package/build/internal/EntityLoadInterfaces.js.map +1 -1
  37. package/build/internal/EntityTableDataCoordinator.d.ts +2 -0
  38. package/build/internal/EntityTableDataCoordinator.js +2 -0
  39. package/build/internal/EntityTableDataCoordinator.js.map +1 -1
  40. package/build/internal/ReadThroughEntityCache.d.ts +8 -0
  41. package/build/internal/ReadThroughEntityCache.js +5 -0
  42. package/build/internal/ReadThroughEntityCache.js.map +1 -1
  43. package/build/internal/SingleFieldHolder.d.ts +7 -0
  44. package/build/internal/SingleFieldHolder.js +7 -0
  45. package/build/internal/SingleFieldHolder.js.map +1 -1
  46. package/build/metrics/EntityMetricsUtils.d.ts +4 -3
  47. package/build/metrics/EntityMetricsUtils.js +6 -3
  48. package/build/metrics/EntityMetricsUtils.js.map +1 -1
  49. package/build/metrics/IEntityMetricsAdapter.d.ts +21 -0
  50. package/build/metrics/IEntityMetricsAdapter.js.map +1 -1
  51. package/build/tsconfig.build.tsbuildinfo +1 -1
  52. package/build/utils/EntityCreationUtils.d.ts +14 -0
  53. package/build/utils/EntityCreationUtils.js +57 -0
  54. package/build/utils/EntityCreationUtils.js.map +1 -0
  55. package/package.json +13 -13
  56. package/src/AuthorizationResultBasedEntityLoader.ts +7 -1
  57. package/src/AuthorizationResultBasedEntityMutator.ts +133 -15
  58. package/src/EntityLoaderUtils.ts +43 -12
  59. package/src/EntityQueryContext.ts +68 -13
  60. package/src/EntityQueryContextProvider.ts +20 -3
  61. package/src/IEntityGenericCacher.ts +2 -2
  62. package/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts +98 -0
  63. package/src/__tests__/EntityQueryContext-test.ts +141 -26
  64. package/src/errors/EntityNotFoundError.ts +51 -4
  65. package/src/errors/__tests__/EntityDatabaseAdapterError-test.ts +26 -0
  66. package/src/index.ts +1 -0
  67. package/src/internal/CompositeFieldHolder.ts +15 -0
  68. package/src/internal/CompositeFieldValueMap.ts +3 -0
  69. package/src/internal/EntityDataManager.ts +170 -10
  70. package/src/internal/EntityFieldTransformationUtils.ts +20 -0
  71. package/src/internal/EntityLoadInterfaces.ts +8 -0
  72. package/src/internal/EntityTableDataCoordinator.ts +2 -0
  73. package/src/internal/ReadThroughEntityCache.ts +8 -0
  74. package/src/internal/SingleFieldHolder.ts +7 -0
  75. package/src/internal/__tests__/EntityDataManager-test.ts +708 -186
  76. package/src/metrics/EntityMetricsUtils.ts +7 -0
  77. package/src/metrics/IEntityMetricsAdapter.ts +27 -0
  78. package/src/utils/EntityCreationUtils.ts +143 -0
  79. package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +13 -1
  80. package/src/utils/__tests__/EntityCreationUtils-test.ts +354 -0
@@ -4,14 +4,15 @@ import {
4
4
  when,
5
5
  anything,
6
6
  verify,
7
- objectContaining,
8
7
  spy,
9
8
  anyString,
10
9
  resetCalls,
11
10
  deepEqual,
11
+ anyNumber,
12
12
  } from 'ts-mockito';
13
13
 
14
14
  import EntityDatabaseAdapter from '../../EntityDatabaseAdapter';
15
+ import { TransactionalDataLoaderMode } from '../../EntityQueryContext';
15
16
  import IEntityMetricsAdapter, {
16
17
  EntityMetricsLoadType,
17
18
  IncrementLoadCountEventType,
@@ -310,6 +311,237 @@ describe(EntityDataManager, () => {
310
311
  cacheSpy.mockReset();
311
312
  });
312
313
 
314
+ it('loads and in-memory batches (dataloader) loads in transaction when enabled with TransactionalDataLoaderMode.ENABLED_BATCH_ONLY and does not read from cache for transactions and nested transactions', async () => {
315
+ const objects = getObjects();
316
+ const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
317
+ testEntityConfiguration,
318
+ objects,
319
+ );
320
+ const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
321
+ testEntityConfiguration,
322
+ dataStore,
323
+ );
324
+ const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
325
+ const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
326
+ const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
327
+ const entityDataManager = new EntityDataManager(
328
+ databaseAdapter,
329
+ entityCache,
330
+ new StubQueryContextProvider(),
331
+ new NoOpEntityMetricsAdapter(),
332
+ TestEntity.name,
333
+ );
334
+
335
+ const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
336
+ const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
337
+
338
+ const [entityData, entityData2, entityData3, entityData4] =
339
+ await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
340
+ const [entityData, entityData2] = await Promise.all([
341
+ entityDataManager.loadManyEqualingAsync(
342
+ queryContext,
343
+ new SingleFieldHolder('stringField'),
344
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
345
+ ),
346
+ entityDataManager.loadManyEqualingAsync(
347
+ queryContext,
348
+ new SingleFieldHolder('stringField'),
349
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
350
+ ),
351
+ ]);
352
+ const [entityData3, entityData4] = await queryContext.runInNestedTransactionAsync(
353
+ async (innerQueryContext) => {
354
+ const entityData3 = await entityDataManager.loadManyEqualingAsync(
355
+ innerQueryContext,
356
+ new SingleFieldHolder('stringField'),
357
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
358
+ );
359
+
360
+ const entityData4 = await queryContext.runInNestedTransactionAsync(
361
+ async (innerInnerQueryContext) => {
362
+ return await entityDataManager.loadManyEqualingAsync(
363
+ innerInnerQueryContext,
364
+ new SingleFieldHolder('stringField'),
365
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
366
+ );
367
+ },
368
+ );
369
+
370
+ return [entityData3, entityData4];
371
+ },
372
+ );
373
+ return [entityData, entityData2, entityData3, entityData4];
374
+ });
375
+
376
+ // entityData, entityData3 (new nested transaction), and entityData4 (new nested transaction) loads should all need to call the database
377
+ // entityData and entityData2 should be batched to one db load call
378
+ expect(dbSpy).toHaveBeenCalledTimes(3);
379
+ expect(cacheSpy).toHaveBeenCalledTimes(0);
380
+
381
+ expect(entityData).toMatchObject(entityData2);
382
+ expect(entityData2).toMatchObject(entityData3);
383
+ expect(entityData3).toMatchObject(entityData4);
384
+ expect(entityData.get(new SingleFieldValueHolder('hello'))).toHaveLength(2);
385
+ expect(entityData.get(new SingleFieldValueHolder('world'))).toHaveLength(1);
386
+
387
+ dbSpy.mockReset();
388
+ cacheSpy.mockReset();
389
+ });
390
+
391
+ it('loads and in-memory caches (dataloader) loads in transaction when enabled with TransactionalDataLoaderMode.ENABLED and does not read from cache for transactions and nested transactions', async () => {
392
+ const objects = getObjects();
393
+ const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
394
+ testEntityConfiguration,
395
+ objects,
396
+ );
397
+ const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
398
+ testEntityConfiguration,
399
+ dataStore,
400
+ );
401
+ const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
402
+ const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
403
+ const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
404
+ const entityDataManager = new EntityDataManager(
405
+ databaseAdapter,
406
+ entityCache,
407
+ new StubQueryContextProvider(),
408
+ new NoOpEntityMetricsAdapter(),
409
+ TestEntity.name,
410
+ );
411
+
412
+ const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
413
+ const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
414
+
415
+ const [entityData, entityData2, entityData3, entityData4] =
416
+ await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
417
+ const entityData = await entityDataManager.loadManyEqualingAsync(
418
+ queryContext,
419
+ new SingleFieldHolder('stringField'),
420
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
421
+ );
422
+ const entityData2 = await entityDataManager.loadManyEqualingAsync(
423
+ queryContext,
424
+ new SingleFieldHolder('stringField'),
425
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
426
+ );
427
+ const [entityData3, entityData4] = await queryContext.runInNestedTransactionAsync(
428
+ async (innerQueryContext) => {
429
+ const entityData3 = await entityDataManager.loadManyEqualingAsync(
430
+ innerQueryContext,
431
+ new SingleFieldHolder('stringField'),
432
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
433
+ );
434
+
435
+ const entityData4 = await queryContext.runInNestedTransactionAsync(
436
+ async (innerInnerQueryContext) => {
437
+ return await entityDataManager.loadManyEqualingAsync(
438
+ innerInnerQueryContext,
439
+ new SingleFieldHolder('stringField'),
440
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
441
+ );
442
+ },
443
+ );
444
+
445
+ return [entityData3, entityData4];
446
+ },
447
+ );
448
+ return [entityData, entityData2, entityData3, entityData4];
449
+ });
450
+
451
+ // entityData, entityData3 (new nested transaction), and entityData4 (new nested transaction) loads should all need to call the database
452
+ // entityData2 load should be cached in the dataloader
453
+ expect(dbSpy).toHaveBeenCalledTimes(3);
454
+ expect(cacheSpy).toHaveBeenCalledTimes(0);
455
+
456
+ expect(entityData).toMatchObject(entityData2);
457
+ expect(entityData2).toMatchObject(entityData3);
458
+ expect(entityData3).toMatchObject(entityData4);
459
+ expect(entityData.get(new SingleFieldValueHolder('hello'))).toHaveLength(2);
460
+ expect(entityData.get(new SingleFieldValueHolder('world'))).toHaveLength(1);
461
+
462
+ dbSpy.mockReset();
463
+ cacheSpy.mockReset();
464
+ });
465
+
466
+ it('loads and does not in-memory cache (dataloader) loads in transaction when disabled', async () => {
467
+ const objects = getObjects();
468
+ const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
469
+ testEntityConfiguration,
470
+ objects,
471
+ );
472
+ const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
473
+ testEntityConfiguration,
474
+ dataStore,
475
+ );
476
+ const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
477
+ const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
478
+ const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
479
+ const entityDataManager = new EntityDataManager(
480
+ databaseAdapter,
481
+ entityCache,
482
+ new StubQueryContextProvider(),
483
+ new NoOpEntityMetricsAdapter(),
484
+ TestEntity.name,
485
+ );
486
+
487
+ const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
488
+ const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
489
+
490
+ const [entityData, entityData2, entityData3, entityData4] =
491
+ await new StubQueryContextProvider().runInTransactionAsync(
492
+ async (queryContext) => {
493
+ const entityData = await entityDataManager.loadManyEqualingAsync(
494
+ queryContext,
495
+ new SingleFieldHolder('stringField'),
496
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
497
+ );
498
+ const entityData2 = await entityDataManager.loadManyEqualingAsync(
499
+ queryContext,
500
+ new SingleFieldHolder('stringField'),
501
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
502
+ );
503
+ const [entityData3, entityData4] = await queryContext.runInNestedTransactionAsync(
504
+ async (innerQueryContext) => {
505
+ const entityData3 = await entityDataManager.loadManyEqualingAsync(
506
+ innerQueryContext,
507
+ new SingleFieldHolder('stringField'),
508
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
509
+ );
510
+
511
+ const entityData4 = await queryContext.runInNestedTransactionAsync(
512
+ async (innerInnerQueryContext) => {
513
+ return await entityDataManager.loadManyEqualingAsync(
514
+ innerInnerQueryContext,
515
+ new SingleFieldHolder('stringField'),
516
+ [new SingleFieldValueHolder('hello'), new SingleFieldValueHolder('world')],
517
+ );
518
+ },
519
+ );
520
+
521
+ return [entityData3, entityData4];
522
+ },
523
+ );
524
+ return [entityData, entityData2, entityData3, entityData4];
525
+ },
526
+ {
527
+ transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED,
528
+ },
529
+ );
530
+
531
+ // entityData, entityData2, entityData3 (new nested transaction), and entityData4 (new nested transaction) loads should all need to call the database
532
+ expect(dbSpy).toHaveBeenCalledTimes(4);
533
+ expect(cacheSpy).toHaveBeenCalledTimes(0);
534
+
535
+ expect(entityData).toMatchObject(entityData2);
536
+ expect(entityData2).toMatchObject(entityData3);
537
+ expect(entityData3).toMatchObject(entityData4);
538
+ expect(entityData.get(new SingleFieldValueHolder('hello'))).toHaveLength(2);
539
+ expect(entityData.get(new SingleFieldValueHolder('world'))).toHaveLength(1);
540
+
541
+ dbSpy.mockReset();
542
+ cacheSpy.mockReset();
543
+ });
544
+
313
545
  it('invalidates objects', async () => {
314
546
  const objects = getObjects();
315
547
  const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
@@ -412,7 +644,141 @@ describe(EntityDataManager, () => {
412
644
  cacheSpy.mockReset();
413
645
  });
414
646
 
415
- it('loads only from DB when in transaction', async () => {
647
+ it('invalidates transactions and nested transactions correctly', async () => {
648
+ const objects = getObjects();
649
+ const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
650
+ testEntityConfiguration,
651
+ objects,
652
+ );
653
+ const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
654
+ testEntityConfiguration,
655
+ dataStore,
656
+ );
657
+ const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
658
+ const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
659
+ const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
660
+ const entityDataManager = new EntityDataManager(
661
+ databaseAdapter,
662
+ entityCache,
663
+ new StubQueryContextProvider(),
664
+ new NoOpEntityMetricsAdapter(),
665
+ TestEntity.name,
666
+ );
667
+
668
+ await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
669
+ const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1]!;
670
+
671
+ const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
672
+ const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
673
+
674
+ await entityDataManager.loadManyEqualingAsync(
675
+ queryContext,
676
+ new SingleFieldHolder('testIndexedField'),
677
+ [new SingleFieldValueHolder(objectInQuestion['testIndexedField'])],
678
+ );
679
+ entityDataManager.invalidateKeyValuePairsForTransaction(queryContext, [
680
+ [
681
+ new SingleFieldHolder('testIndexedField'),
682
+ new SingleFieldValueHolder(objectInQuestion['testIndexedField']),
683
+ ],
684
+ ]);
685
+ await entityDataManager.loadManyEqualingAsync(
686
+ queryContext,
687
+ new SingleFieldHolder('testIndexedField'),
688
+ [new SingleFieldValueHolder(objectInQuestion['testIndexedField'])],
689
+ );
690
+
691
+ expect(dbSpy).toHaveBeenCalledTimes(2);
692
+ expect(cacheSpy).toHaveBeenCalledTimes(0);
693
+
694
+ dbSpy.mockClear();
695
+ cacheSpy.mockClear();
696
+
697
+ await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
698
+ await entityDataManager.loadManyEqualingAsync(
699
+ innerQueryContext,
700
+ new SingleFieldHolder('testIndexedField'),
701
+ [new SingleFieldValueHolder(objectInQuestion['testIndexedField'])],
702
+ );
703
+ entityDataManager.invalidateKeyValuePairsForTransaction(innerQueryContext, [
704
+ [
705
+ new SingleFieldHolder('testIndexedField'),
706
+ new SingleFieldValueHolder(objectInQuestion['testIndexedField']),
707
+ ],
708
+ ]);
709
+ await entityDataManager.loadManyEqualingAsync(
710
+ innerQueryContext,
711
+ new SingleFieldHolder('testIndexedField'),
712
+ [new SingleFieldValueHolder(objectInQuestion['testIndexedField'])],
713
+ );
714
+
715
+ expect(dbSpy).toHaveBeenCalledTimes(2);
716
+ expect(cacheSpy).toHaveBeenCalledTimes(0);
717
+
718
+ dbSpy.mockClear();
719
+ cacheSpy.mockClear();
720
+ });
721
+ });
722
+ });
723
+
724
+ it('does not use transactional dataloader when disabled', async () => {
725
+ const objects = getObjects();
726
+ const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
727
+ testEntityConfiguration,
728
+ objects,
729
+ );
730
+ const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
731
+ testEntityConfiguration,
732
+ dataStore,
733
+ );
734
+ const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
735
+ const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
736
+ const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
737
+ const entityDataManager = new EntityDataManager(
738
+ databaseAdapter,
739
+ entityCache,
740
+ new StubQueryContextProvider(),
741
+ new NoOpEntityMetricsAdapter(),
742
+ TestEntity.name,
743
+ );
744
+
745
+ await new StubQueryContextProvider().runInTransactionAsync(
746
+ async (queryContext) => {
747
+ const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1]!;
748
+
749
+ const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
750
+ const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
751
+
752
+ await entityDataManager.loadManyEqualingAsync(
753
+ queryContext,
754
+ new SingleFieldHolder('testIndexedField'),
755
+ [new SingleFieldValueHolder(objectInQuestion['testIndexedField'])],
756
+ );
757
+ entityDataManager.invalidateKeyValuePairsForTransaction(queryContext, [
758
+ [
759
+ new SingleFieldHolder('testIndexedField'),
760
+ new SingleFieldValueHolder(objectInQuestion['testIndexedField']),
761
+ ],
762
+ ]);
763
+ await entityDataManager.loadManyEqualingAsync(
764
+ queryContext,
765
+ new SingleFieldHolder('testIndexedField'),
766
+ [new SingleFieldValueHolder(objectInQuestion['testIndexedField'])],
767
+ );
768
+
769
+ expect(dbSpy).toHaveBeenCalledTimes(2);
770
+ expect(cacheSpy).toHaveBeenCalledTimes(0);
771
+
772
+ dbSpy.mockClear();
773
+ cacheSpy.mockClear();
774
+ },
775
+ { transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED },
776
+ );
777
+
778
+ expect(entityDataManager['transactionalDataLoaders'].size).toBe(0);
779
+ });
780
+
781
+ it('does not load from cache when in transaction', async () => {
416
782
  const objects = getObjects();
417
783
  const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
418
784
  testEntityConfiguration,
@@ -533,194 +899,350 @@ describe(EntityDataManager, () => {
533
899
  ).rejects.toThrow();
534
900
  });
535
901
 
536
- it('records metrics appropriately', async () => {
537
- const metricsAdapterMock = mock<IEntityMetricsAdapter>();
538
- const metricsAdapter = instance(metricsAdapterMock);
539
-
540
- const objects = getObjects();
541
- const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
542
- testEntityConfiguration,
543
- objects,
544
- );
545
- const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
546
- testEntityConfiguration,
547
- dataStore,
548
- );
549
- const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
550
- const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
551
- const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
552
- const entityDataManager = new EntityDataManager(
553
- databaseAdapter,
554
- entityCache,
555
- new StubQueryContextProvider(),
556
- metricsAdapter,
557
- TestEntity.name,
558
- );
559
- const queryContext = new StubQueryContextProvider().getQueryContext();
560
-
561
- // make call to loadManyByFieldEqualingAsync to populate cache and dataloader, ensure metrics are recorded
562
- // for dataloader, cache, and database
563
- await entityDataManager.loadManyEqualingAsync(
564
- queryContext,
565
- new SingleFieldHolder('testIndexedField'),
566
- [new SingleFieldValueHolder('unique1')],
567
- );
568
- verify(
569
- metricsAdapterMock.logDataManagerLoadEvent(
570
- objectContaining({
571
- type: EntityMetricsLoadType.LOAD_MANY,
572
- entityClassName: TestEntity.name,
573
- count: 1,
574
- }),
575
- ),
576
- ).once();
577
- verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).thrice();
578
- verify(
579
- metricsAdapterMock.incrementDataManagerLoadCount(
580
- deepEqual({
581
- type: IncrementLoadCountEventType.DATALOADER,
582
- fieldValueCount: 1,
583
- entityClassName: TestEntity.name,
584
- loadType: EntityLoadMethodType.SINGLE,
585
- }),
586
- ),
587
- ).once();
588
- verify(
589
- metricsAdapterMock.incrementDataManagerLoadCount(
590
- deepEqual({
591
- type: IncrementLoadCountEventType.CACHE,
592
- fieldValueCount: 1,
593
- entityClassName: TestEntity.name,
594
- loadType: EntityLoadMethodType.SINGLE,
595
- }),
596
- ),
597
- ).once();
598
- verify(
599
- metricsAdapterMock.incrementDataManagerLoadCount(
600
- deepEqual({
601
- type: IncrementLoadCountEventType.DATABASE,
602
- fieldValueCount: 1,
603
- entityClassName: TestEntity.name,
604
- loadType: EntityLoadMethodType.SINGLE,
605
- }),
606
- ),
607
- ).once();
608
-
609
- resetCalls(metricsAdapterMock);
610
-
611
- // make second call to loadManyByFieldEqualingAsync, ensure metrics are only recorded for dataloader since
612
- // entity is in local dataloader
613
- await entityDataManager.loadManyEqualingAsync(
614
- queryContext,
615
- new SingleFieldHolder('testIndexedField'),
616
- [new SingleFieldValueHolder('unique1')],
617
- );
618
- verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).once();
619
- verify(
620
- metricsAdapterMock.incrementDataManagerLoadCount(
621
- deepEqual({
622
- type: IncrementLoadCountEventType.DATALOADER,
623
- fieldValueCount: 1,
624
- entityClassName: TestEntity.name,
625
- loadType: EntityLoadMethodType.SINGLE,
626
- }),
627
- ),
628
- ).once();
629
-
630
- resetCalls(metricsAdapterMock);
631
-
632
- // make third call in new data manager but query two keys, ensure only one of the keys is fetched
633
- // from the database and the other is fetched from the cache
634
- const entityCache2 = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
635
- const entityDataManager2 = new EntityDataManager(
636
- databaseAdapter,
637
- entityCache2,
638
- new StubQueryContextProvider(),
639
- metricsAdapter,
640
- TestEntity.name,
641
- );
642
- await entityDataManager2.loadManyEqualingAsync(
643
- queryContext,
644
- new SingleFieldHolder('testIndexedField'),
645
- [new SingleFieldValueHolder('unique1'), new SingleFieldValueHolder('unique2')],
646
- );
647
- verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).thrice();
648
- verify(
649
- metricsAdapterMock.incrementDataManagerLoadCount(
650
- deepEqual({
651
- type: IncrementLoadCountEventType.DATALOADER,
652
- fieldValueCount: 2,
653
- entityClassName: TestEntity.name,
654
- loadType: EntityLoadMethodType.SINGLE,
655
- }),
656
- ),
657
- ).once();
658
- verify(
659
- metricsAdapterMock.incrementDataManagerLoadCount(
660
- deepEqual({
661
- type: IncrementLoadCountEventType.CACHE,
662
- fieldValueCount: 2,
663
- entityClassName: TestEntity.name,
664
- loadType: EntityLoadMethodType.SINGLE,
665
- }),
666
- ),
667
- ).once();
668
- verify(
669
- metricsAdapterMock.incrementDataManagerLoadCount(
670
- deepEqual({
671
- type: IncrementLoadCountEventType.DATABASE,
672
- fieldValueCount: 1,
673
- entityClassName: TestEntity.name,
674
- loadType: EntityLoadMethodType.SINGLE,
675
- }),
676
- ),
677
- ).once();
678
-
679
- resetCalls(metricsAdapterMock);
902
+ describe('metrics', () => {
903
+ it('records metrics appropriately outside of transactions', async () => {
904
+ const metricsAdapterMock = mock<IEntityMetricsAdapter>();
905
+ const metricsAdapter = instance(metricsAdapterMock);
906
+
907
+ const objects = getObjects();
908
+ const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
909
+ testEntityConfiguration,
910
+ objects,
911
+ );
912
+ const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
913
+ testEntityConfiguration,
914
+ dataStore,
915
+ );
916
+ const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
917
+ const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
918
+ const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
919
+ const entityDataManager = new EntityDataManager(
920
+ databaseAdapter,
921
+ entityCache,
922
+ new StubQueryContextProvider(),
923
+ metricsAdapter,
924
+ TestEntity.name,
925
+ );
926
+ const queryContext = new StubQueryContextProvider().getQueryContext();
927
+
928
+ // make call to loadManyByFieldEqualingAsync to populate cache and dataloader, ensure metrics are recorded
929
+ // for dataloader, cache, and database
930
+ await entityDataManager.loadManyEqualingAsync(
931
+ queryContext,
932
+ new SingleFieldHolder('testIndexedField'),
933
+ [new SingleFieldValueHolder('unique1')],
934
+ );
935
+ verify(
936
+ metricsAdapterMock.logDataManagerLoadEvent(
937
+ deepEqual({
938
+ type: EntityMetricsLoadType.LOAD_MANY,
939
+ isInTransaction: false,
940
+ entityClassName: TestEntity.name,
941
+ duration: anyNumber(),
942
+ count: 1,
943
+ }),
944
+ ),
945
+ ).once();
946
+ verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).thrice();
947
+ verify(
948
+ metricsAdapterMock.incrementDataManagerLoadCount(
949
+ deepEqual({
950
+ type: IncrementLoadCountEventType.DATALOADER,
951
+ isInTransaction: false,
952
+ fieldValueCount: 1,
953
+ entityClassName: TestEntity.name,
954
+ loadType: EntityLoadMethodType.SINGLE,
955
+ }),
956
+ ),
957
+ ).once();
958
+ verify(
959
+ metricsAdapterMock.incrementDataManagerLoadCount(
960
+ deepEqual({
961
+ type: IncrementLoadCountEventType.CACHE,
962
+ isInTransaction: false,
963
+ fieldValueCount: 1,
964
+ entityClassName: TestEntity.name,
965
+ loadType: EntityLoadMethodType.SINGLE,
966
+ }),
967
+ ),
968
+ ).once();
969
+ verify(
970
+ metricsAdapterMock.incrementDataManagerLoadCount(
971
+ deepEqual({
972
+ type: IncrementLoadCountEventType.DATABASE,
973
+ isInTransaction: false,
974
+ fieldValueCount: 1,
975
+ entityClassName: TestEntity.name,
976
+ loadType: EntityLoadMethodType.SINGLE,
977
+ }),
978
+ ),
979
+ ).once();
680
980
 
681
- await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
682
- queryContext,
683
- [
684
- {
685
- fieldName: 'testIndexedField',
686
- fieldValue: 'unique1',
687
- },
688
- ],
689
- {},
690
- );
691
- verify(
692
- metricsAdapterMock.logDataManagerLoadEvent(
693
- objectContaining({
694
- type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION,
695
- entityClassName: TestEntity.name,
696
- count: 1,
697
- }),
698
- ),
699
- ).once();
981
+ resetCalls(metricsAdapterMock);
700
982
 
701
- resetCalls(metricsAdapterMock);
983
+ // make second call to loadManyByFieldEqualingAsync, ensure metrics are only recorded for dataloader since
984
+ // entity is in local dataloader
985
+ await entityDataManager.loadManyEqualingAsync(
986
+ queryContext,
987
+ new SingleFieldHolder('testIndexedField'),
988
+ [new SingleFieldValueHolder('unique1')],
989
+ );
990
+ verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).once();
991
+ verify(
992
+ metricsAdapterMock.incrementDataManagerLoadCount(
993
+ deepEqual({
994
+ type: IncrementLoadCountEventType.DATALOADER,
995
+ isInTransaction: false,
996
+ fieldValueCount: 1,
997
+ entityClassName: TestEntity.name,
998
+ loadType: EntityLoadMethodType.SINGLE,
999
+ }),
1000
+ ),
1001
+ ).once();
1002
+
1003
+ resetCalls(metricsAdapterMock);
1004
+
1005
+ // make third call in new data manager but query two keys, ensure only one of the keys is fetched
1006
+ // from the database and the other is fetched from the cache
1007
+ const entityCache2 = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
1008
+ const entityDataManager2 = new EntityDataManager(
1009
+ databaseAdapter,
1010
+ entityCache2,
1011
+ new StubQueryContextProvider(),
1012
+ metricsAdapter,
1013
+ TestEntity.name,
1014
+ );
1015
+ await entityDataManager2.loadManyEqualingAsync(
1016
+ queryContext,
1017
+ new SingleFieldHolder('testIndexedField'),
1018
+ [new SingleFieldValueHolder('unique1'), new SingleFieldValueHolder('unique2')],
1019
+ );
1020
+ verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).thrice();
1021
+ verify(
1022
+ metricsAdapterMock.incrementDataManagerLoadCount(
1023
+ deepEqual({
1024
+ type: IncrementLoadCountEventType.DATALOADER,
1025
+ isInTransaction: false,
1026
+ fieldValueCount: 2,
1027
+ entityClassName: TestEntity.name,
1028
+ loadType: EntityLoadMethodType.SINGLE,
1029
+ }),
1030
+ ),
1031
+ ).once();
1032
+ verify(
1033
+ metricsAdapterMock.incrementDataManagerLoadCount(
1034
+ deepEqual({
1035
+ type: IncrementLoadCountEventType.CACHE,
1036
+ isInTransaction: false,
1037
+ fieldValueCount: 2,
1038
+ entityClassName: TestEntity.name,
1039
+ loadType: EntityLoadMethodType.SINGLE,
1040
+ }),
1041
+ ),
1042
+ ).once();
1043
+ verify(
1044
+ metricsAdapterMock.incrementDataManagerLoadCount(
1045
+ deepEqual({
1046
+ type: IncrementLoadCountEventType.DATABASE,
1047
+ isInTransaction: false,
1048
+ fieldValueCount: 1,
1049
+ entityClassName: TestEntity.name,
1050
+ loadType: EntityLoadMethodType.SINGLE,
1051
+ }),
1052
+ ),
1053
+ ).once();
702
1054
 
703
- const databaseAdapterSpy = spy(databaseAdapter);
704
- when(
705
- databaseAdapterSpy.fetchManyByRawWhereClauseAsync(
706
- anything(),
707
- anyString(),
708
- anything(),
709
- anything(),
710
- ),
711
- ).thenResolve([]);
712
- await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {});
713
- verify(
714
- metricsAdapterMock.logDataManagerLoadEvent(
715
- objectContaining({
716
- type: EntityMetricsLoadType.LOAD_MANY_RAW,
717
- entityClassName: TestEntity.name,
718
- count: 0,
719
- }),
720
- ),
721
- ).once();
1055
+ resetCalls(metricsAdapterMock);
722
1056
 
723
- verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never();
1057
+ await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
1058
+ queryContext,
1059
+ [
1060
+ {
1061
+ fieldName: 'testIndexedField',
1062
+ fieldValue: 'unique1',
1063
+ },
1064
+ ],
1065
+ {},
1066
+ );
1067
+ verify(
1068
+ metricsAdapterMock.logDataManagerLoadEvent(
1069
+ deepEqual({
1070
+ type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION,
1071
+ isInTransaction: false,
1072
+ entityClassName: TestEntity.name,
1073
+ duration: anyNumber(),
1074
+ count: 1,
1075
+ }),
1076
+ ),
1077
+ ).once();
1078
+
1079
+ resetCalls(metricsAdapterMock);
1080
+
1081
+ const databaseAdapterSpy = spy(databaseAdapter);
1082
+ when(
1083
+ databaseAdapterSpy.fetchManyByRawWhereClauseAsync(
1084
+ anything(),
1085
+ anyString(),
1086
+ anything(),
1087
+ anything(),
1088
+ ),
1089
+ ).thenResolve([]);
1090
+ await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {});
1091
+ verify(
1092
+ metricsAdapterMock.logDataManagerLoadEvent(
1093
+ deepEqual({
1094
+ type: EntityMetricsLoadType.LOAD_MANY_RAW,
1095
+ isInTransaction: false,
1096
+ entityClassName: TestEntity.name,
1097
+ duration: anyNumber(),
1098
+ count: 0,
1099
+ }),
1100
+ ),
1101
+ ).once();
1102
+
1103
+ verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never();
1104
+ });
1105
+
1106
+ it('records metrics appropriately inside of transactions', async () => {
1107
+ const metricsAdapterMock = mock<IEntityMetricsAdapter>();
1108
+ const metricsAdapter = instance(metricsAdapterMock);
1109
+
1110
+ const objects = getObjects();
1111
+ const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
1112
+ testEntityConfiguration,
1113
+ objects,
1114
+ );
1115
+ const databaseAdapter = new StubDatabaseAdapter<TestFields, 'customIdField'>(
1116
+ testEntityConfiguration,
1117
+ dataStore,
1118
+ );
1119
+ const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
1120
+ const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
1121
+ const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
1122
+ const entityDataManager = new EntityDataManager(
1123
+ databaseAdapter,
1124
+ entityCache,
1125
+ new StubQueryContextProvider(),
1126
+ metricsAdapter,
1127
+ TestEntity.name,
1128
+ );
1129
+
1130
+ await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
1131
+ // make call to loadManyByFieldEqualingAsync to populate cache and dataloader, ensure metrics are recorded
1132
+ // for dataloader, cache, and database
1133
+ await entityDataManager.loadManyEqualingAsync(
1134
+ queryContext,
1135
+ new SingleFieldHolder('testIndexedField'),
1136
+ [new SingleFieldValueHolder('unique1')],
1137
+ );
1138
+ verify(
1139
+ metricsAdapterMock.logDataManagerLoadEvent(
1140
+ deepEqual({
1141
+ type: EntityMetricsLoadType.LOAD_MANY,
1142
+ isInTransaction: true,
1143
+ entityClassName: TestEntity.name,
1144
+ duration: anyNumber(),
1145
+ count: 1,
1146
+ }),
1147
+ ),
1148
+ ).once();
1149
+ verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).twice();
1150
+ verify(
1151
+ metricsAdapterMock.incrementDataManagerLoadCount(
1152
+ deepEqual({
1153
+ type: IncrementLoadCountEventType.DATALOADER,
1154
+ isInTransaction: true,
1155
+ fieldValueCount: 1,
1156
+ entityClassName: TestEntity.name,
1157
+ loadType: EntityLoadMethodType.SINGLE,
1158
+ }),
1159
+ ),
1160
+ ).once();
1161
+ verify(
1162
+ metricsAdapterMock.incrementDataManagerLoadCount(
1163
+ deepEqual({
1164
+ type: IncrementLoadCountEventType.DATABASE,
1165
+ isInTransaction: true,
1166
+ fieldValueCount: 1,
1167
+ entityClassName: TestEntity.name,
1168
+ loadType: EntityLoadMethodType.SINGLE,
1169
+ }),
1170
+ ),
1171
+ ).once();
1172
+
1173
+ resetCalls(metricsAdapterMock);
1174
+
1175
+ // make second call to loadManyByFieldEqualingAsync, ensure metrics are only recorded for dataloader since
1176
+ // entity is in local dataloader
1177
+ await entityDataManager.loadManyEqualingAsync(
1178
+ queryContext,
1179
+ new SingleFieldHolder('testIndexedField'),
1180
+ [new SingleFieldValueHolder('unique1')],
1181
+ );
1182
+ verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).once();
1183
+ verify(
1184
+ metricsAdapterMock.incrementDataManagerLoadCount(
1185
+ deepEqual({
1186
+ type: IncrementLoadCountEventType.DATALOADER,
1187
+ isInTransaction: true,
1188
+ fieldValueCount: 1,
1189
+ entityClassName: TestEntity.name,
1190
+ loadType: EntityLoadMethodType.SINGLE,
1191
+ }),
1192
+ ),
1193
+ ).once();
1194
+
1195
+ resetCalls(metricsAdapterMock);
1196
+
1197
+ await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
1198
+ queryContext,
1199
+ [
1200
+ {
1201
+ fieldName: 'testIndexedField',
1202
+ fieldValue: 'unique1',
1203
+ },
1204
+ ],
1205
+ {},
1206
+ );
1207
+ verify(
1208
+ metricsAdapterMock.logDataManagerLoadEvent(
1209
+ deepEqual({
1210
+ type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION,
1211
+ isInTransaction: true,
1212
+ entityClassName: TestEntity.name,
1213
+ duration: anyNumber(),
1214
+ count: 1,
1215
+ }),
1216
+ ),
1217
+ ).once();
1218
+
1219
+ resetCalls(metricsAdapterMock);
1220
+
1221
+ const databaseAdapterSpy = spy(databaseAdapter);
1222
+ when(
1223
+ databaseAdapterSpy.fetchManyByRawWhereClauseAsync(
1224
+ anything(),
1225
+ anyString(),
1226
+ anything(),
1227
+ anything(),
1228
+ ),
1229
+ ).thenResolve([]);
1230
+ await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {});
1231
+ verify(
1232
+ metricsAdapterMock.logDataManagerLoadEvent(
1233
+ deepEqual({
1234
+ type: EntityMetricsLoadType.LOAD_MANY_RAW,
1235
+ isInTransaction: true,
1236
+ entityClassName: TestEntity.name,
1237
+ duration: anyNumber(),
1238
+ count: 0,
1239
+ }),
1240
+ ),
1241
+ ).once();
1242
+
1243
+ verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never();
1244
+ });
1245
+ });
724
1246
  });
725
1247
 
726
1248
  it('throws when a single value load-by value is null or undefined', async () => {