From 733b3636d1e645c7ccfea8fd11cf8e903f465f0f Mon Sep 17 00:00:00 2001 From: Will Schurman Date: Fri, 30 Jan 2026 18:04:58 -0700 Subject: [PATCH] chore: move knex-specific loader logic out of EntityDataManager --- ...uthorizationResultBasedKnexEntityLoader.ts | 8 +- packages/entity/src/EntityCompanion.ts | 2 +- .../entity/src/KnexEntityLoaderFactory.ts | 4 +- ...izationResultBasedKnexEntityLoader-test.ts | 26 +- packages/entity/src/index.ts | 1 + .../entity/src/internal/EntityDataManager.ts | 70 +---- .../src/internal/EntityKnexDataManager.ts | 84 ++++++ .../internal/EntityTableDataCoordinator.ts | 7 + .../__tests__/EntityDataManager-test.ts | 151 ----------- .../__tests__/EntityKnexDataManager-test.ts | 241 ++++++++++++++++++ 10 files changed, 364 insertions(+), 230 deletions(-) create mode 100644 packages/entity/src/internal/EntityKnexDataManager.ts create mode 100644 packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts diff --git a/packages/entity/src/AuthorizationResultBasedKnexEntityLoader.ts b/packages/entity/src/AuthorizationResultBasedKnexEntityLoader.ts index 1bf25b13a..b310d9d99 100644 --- a/packages/entity/src/AuthorizationResultBasedKnexEntityLoader.ts +++ b/packages/entity/src/AuthorizationResultBasedKnexEntityLoader.ts @@ -11,7 +11,7 @@ import { EntityPrivacyPolicy } from './EntityPrivacyPolicy'; import { EntityQueryContext } from './EntityQueryContext'; import { ReadonlyEntity } from './ReadonlyEntity'; import { ViewerContext } from './ViewerContext'; -import { EntityDataManager } from './internal/EntityDataManager'; +import { EntityKnexDataManager } from './internal/EntityKnexDataManager'; import { IEntityMetricsAdapter } from './metrics/IEntityMetricsAdapter'; /** @@ -36,7 +36,7 @@ export class AuthorizationResultBasedKnexEntityLoader< > { constructor( private readonly queryContext: EntityQueryContext, - private readonly dataManager: EntityDataManager, + private readonly knexDataManager: EntityKnexDataManager, protected readonly metricsAdapter: IEntityMetricsAdapter, public readonly utils: EntityLoaderUtils< TFields, @@ -80,7 +80,7 @@ export class AuthorizationResultBasedKnexEntityLoader< this.utils.validateFieldAndValues(fieldEqualityOperand.fieldName, fieldValues); } - const fieldObjects = await this.dataManager.loadManyByFieldEqualityConjunctionAsync( + const fieldObjects = await this.knexDataManager.loadManyByFieldEqualityConjunctionAsync( this.queryContext, fieldEqualityOperands, querySelectionModifiers, @@ -98,7 +98,7 @@ export class AuthorizationResultBasedKnexEntityLoader< bindings: any[] | object, querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw = {}, ): Promise[]> { - const fieldObjects = await this.dataManager.loadManyByRawWhereClauseAsync( + const fieldObjects = await this.knexDataManager.loadManyByRawWhereClauseAsync( this.queryContext, rawWhereClause, bindings, diff --git a/packages/entity/src/EntityCompanion.ts b/packages/entity/src/EntityCompanion.ts index 82cf9be82..97f801732 100644 --- a/packages/entity/src/EntityCompanion.ts +++ b/packages/entity/src/EntityCompanion.ts @@ -87,7 +87,7 @@ export class EntityCompanion< TEntity, TPrivacyPolicy, TSelectedFields - >(this, tableDataCoordinator.dataManager, metricsAdapter); + >(this, tableDataCoordinator.dataManager, tableDataCoordinator.knexDataManager, metricsAdapter); this.entityMutatorFactory = new EntityMutatorFactory( entityCompanionProvider, tableDataCoordinator.entityConfiguration, diff --git a/packages/entity/src/KnexEntityLoaderFactory.ts b/packages/entity/src/KnexEntityLoaderFactory.ts index 6c36d617b..02d34b20b 100644 --- a/packages/entity/src/KnexEntityLoaderFactory.ts +++ b/packages/entity/src/KnexEntityLoaderFactory.ts @@ -6,6 +6,7 @@ import { EntityQueryContext } from './EntityQueryContext'; import { ReadonlyEntity } from './ReadonlyEntity'; import { ViewerContext } from './ViewerContext'; import { EntityDataManager } from './internal/EntityDataManager'; +import { EntityKnexDataManager } from './internal/EntityKnexDataManager'; import { IEntityMetricsAdapter } from './metrics/IEntityMetricsAdapter'; /** @@ -35,6 +36,7 @@ export class KnexEntityLoaderFactory< TSelectedFields >, private readonly dataManager: EntityDataManager, + private readonly knexDataManager: EntityKnexDataManager, protected readonly metricsAdapter: IEntityMetricsAdapter, ) {} @@ -75,7 +77,7 @@ export class KnexEntityLoaderFactory< return new AuthorizationResultBasedKnexEntityLoader( queryContext, - this.dataManager, + this.knexDataManager, this.metricsAdapter, utils, ); diff --git a/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts b/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts index 98c15eba6..2d4934ca9 100644 --- a/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +++ b/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts @@ -9,6 +9,7 @@ import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy'; import { ViewerContext } from '../ViewerContext'; import { enforceResultsAsync } from '../entityUtils'; import { EntityDataManager } from '../internal/EntityDataManager'; +import { EntityKnexDataManager } from '../internal/EntityKnexDataManager'; import { ReadThroughEntityCache } from '../internal/ReadThroughEntityCache'; import { IEntityMetricsAdapter } from '../metrics/IEntityMetricsAdapter'; import { NoCacheStubCacheAdapterProvider } from '../utils/__testfixtures__/StubCacheAdapter'; @@ -90,6 +91,11 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { instance(mock()), TestEntity.name, ); + const knexDataManager = new EntityKnexDataManager( + databaseAdapter, + metricsAdapter, + TestEntity.name, + ); const utils = new EntityLoaderUtils( viewerContext, queryContext, @@ -103,7 +109,7 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { ); const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader( queryContext, - dataManager, + knexDataManager, metricsAdapter, utils, ); @@ -205,6 +211,11 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { instance(mock()), TestEntity.name, ); + const knexDataManager = new EntityKnexDataManager( + databaseAdapter, + metricsAdapter, + TestEntity.name, + ); const utils = new EntityLoaderUtils( viewerContext, queryContext, @@ -218,7 +229,7 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { ); const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader( queryContext, - dataManager, + knexDataManager, metricsAdapter, utils, ); @@ -271,9 +282,10 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { const metricsAdapter = instance(mock()); const queryContext = new StubQueryContextProvider().getQueryContext(); - const dataManagerMock = mock>(EntityDataManager); + const knexDataManagerMock = + mock>(EntityKnexDataManager); when( - dataManagerMock.loadManyByRawWhereClauseAsync( + knexDataManagerMock.loadManyByRawWhereClauseAsync( queryContext, anything(), anything(), @@ -289,6 +301,10 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { nullableField: null, }, ]); + const knexDataManager = instance(knexDataManagerMock); + + // Create a real dataManager for the EntityLoaderUtils + const dataManagerMock = mock>(EntityDataManager); const dataManager = instance(dataManagerMock); const utils = new EntityLoaderUtils( @@ -304,7 +320,7 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { ); const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader( queryContext, - dataManager, + knexDataManager, metricsAdapter, utils, ); diff --git a/packages/entity/src/index.ts b/packages/entity/src/index.ts index 35f695888..d939b4e9e 100644 --- a/packages/entity/src/index.ts +++ b/packages/entity/src/index.ts @@ -64,6 +64,7 @@ export * from './internal/CompositeFieldHolder'; export * from './internal/CompositeFieldValueMap'; export * from './internal/EntityDataManager'; export * from './internal/EntityFieldTransformationUtils'; +export * from './internal/EntityKnexDataManager'; export * from './internal/EntityLoadInterfaces'; export * from './internal/EntityTableDataCoordinator'; export * from './internal/ReadThroughEntityCache'; diff --git a/packages/entity/src/internal/EntityDataManager.ts b/packages/entity/src/internal/EntityDataManager.ts index 9926b7538..8d7b75e95 100644 --- a/packages/entity/src/internal/EntityDataManager.ts +++ b/packages/entity/src/internal/EntityDataManager.ts @@ -1,12 +1,7 @@ import DataLoader from 'dataloader'; import invariant from 'invariant'; -import { - EntityDatabaseAdapter, - FieldEqualityCondition, - QuerySelectionModifiers, - QuerySelectionModifiersWithOrderByRaw, -} from '../EntityDatabaseAdapter'; +import { EntityDatabaseAdapter } from '../EntityDatabaseAdapter'; import { EntityQueryContext, EntityTransactionalQueryContext, @@ -16,10 +11,7 @@ import { EntityQueryContextProvider } from '../EntityQueryContextProvider'; import { partitionErrors } from '../entityUtils'; import { IEntityLoadKey, IEntityLoadValue, LoadPair } from './EntityLoadInterfaces'; import { ReadThroughEntityCache } from './ReadThroughEntityCache'; -import { - timeAndLogLoadEventAsync, - timeAndLogLoadMapEventAsync, -} from '../metrics/EntityMetricsUtils'; +import { timeAndLogLoadMapEventAsync } from '../metrics/EntityMetricsUtils'; import { EntityMetricsLoadType, IEntityMetricsAdapter, @@ -251,64 +243,6 @@ export class EntityDataManager< return mapToReturn; } - /** - * Loads many objects matching the conjunction of where clauses constructed from - * specified field equality operands. - * - * @param queryContext - query context in which to perform the load - * @param fieldEqualityOperands - list of field equality where clause operand specifications - * @param querySelectionModifiers - limit, offset, and orderBy for the query - * @returns array of objects matching the query - */ - async loadManyByFieldEqualityConjunctionAsync( - queryContext: EntityQueryContext, - fieldEqualityOperands: FieldEqualityCondition[], - querySelectionModifiers: QuerySelectionModifiers, - ): Promise[]> { - return await timeAndLogLoadEventAsync( - this.metricsAdapter, - EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, - this.entityClassName, - queryContext, - )( - this.databaseAdapter.fetchManyByFieldEqualityConjunctionAsync( - queryContext, - fieldEqualityOperands, - querySelectionModifiers, - ), - ); - } - - /** - * Loads many objects matching the raw WHERE clause. - * - * @param queryContext - query context in which to perform the load - * @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders - * @param bindings - array of positional bindings or object of named bindings - * @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query - * @returns array of objects matching the query - */ - async loadManyByRawWhereClauseAsync( - queryContext: EntityQueryContext, - rawWhereClause: string, - bindings: any[] | object, - querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw, - ): Promise[]> { - return await timeAndLogLoadEventAsync( - this.metricsAdapter, - EntityMetricsLoadType.LOAD_MANY_RAW, - this.entityClassName, - queryContext, - )( - this.databaseAdapter.fetchManyByRawWhereClauseAsync( - queryContext, - rawWhereClause, - bindings, - querySelectionModifiers, - ), - ); - } - private async invalidateOneAsync< TLoadKey extends IEntityLoadKey, TSerializedLoadValue, diff --git a/packages/entity/src/internal/EntityKnexDataManager.ts b/packages/entity/src/internal/EntityKnexDataManager.ts new file mode 100644 index 000000000..3a18761cb --- /dev/null +++ b/packages/entity/src/internal/EntityKnexDataManager.ts @@ -0,0 +1,84 @@ +import { + EntityDatabaseAdapter, + FieldEqualityCondition, + QuerySelectionModifiers, + QuerySelectionModifiersWithOrderByRaw, +} from '../EntityDatabaseAdapter'; +import { EntityQueryContext } from '../EntityQueryContext'; +import { timeAndLogLoadEventAsync } from '../metrics/EntityMetricsUtils'; +import { EntityMetricsLoadType, IEntityMetricsAdapter } from '../metrics/IEntityMetricsAdapter'; + +/** + * A knex data manager is responsible for handling non-dataloader-based + * database operations. + * + * @internal + */ +export class EntityKnexDataManager< + TFields extends Record, + TIDField extends keyof TFields, +> { + constructor( + private readonly databaseAdapter: EntityDatabaseAdapter, + private readonly metricsAdapter: IEntityMetricsAdapter, + private readonly entityClassName: string, + ) {} + + /** + * Loads many objects matching the conjunction of where clauses constructed from + * specified field equality operands. + * + * @param queryContext - query context in which to perform the load + * @param fieldEqualityOperands - list of field equality where clause operand specifications + * @param querySelectionModifiers - limit, offset, and orderBy for the query + * @returns array of objects matching the query + */ + async loadManyByFieldEqualityConjunctionAsync( + queryContext: EntityQueryContext, + fieldEqualityOperands: FieldEqualityCondition[], + querySelectionModifiers: QuerySelectionModifiers, + ): Promise[]> { + return await timeAndLogLoadEventAsync( + this.metricsAdapter, + EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, + this.entityClassName, + queryContext, + )( + this.databaseAdapter.fetchManyByFieldEqualityConjunctionAsync( + queryContext, + fieldEqualityOperands, + querySelectionModifiers, + ), + ); + } + + /** + * Loads many objects matching the raw WHERE clause. + * + * @param queryContext - query context in which to perform the load + * @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders + * @param bindings - array of positional bindings or object of named bindings + * @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query + * @returns array of objects matching the query + */ + async loadManyByRawWhereClauseAsync( + queryContext: EntityQueryContext, + rawWhereClause: string, + bindings: any[] | object, + querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw, + ): Promise[]> { + return await timeAndLogLoadEventAsync( + this.metricsAdapter, + EntityMetricsLoadType.LOAD_MANY_RAW, + this.entityClassName, + queryContext, + )( + this.databaseAdapter.fetchManyByRawWhereClauseAsync( + queryContext, + rawWhereClause, + bindings, + querySelectionModifiers, + ), + ); + } +} diff --git a/packages/entity/src/internal/EntityTableDataCoordinator.ts b/packages/entity/src/internal/EntityTableDataCoordinator.ts index 821c107e1..5df415b72 100644 --- a/packages/entity/src/internal/EntityTableDataCoordinator.ts +++ b/packages/entity/src/internal/EntityTableDataCoordinator.ts @@ -5,6 +5,7 @@ import { IEntityCacheAdapter } from '../IEntityCacheAdapter'; import { IEntityCacheAdapterProvider } from '../IEntityCacheAdapterProvider'; import { IEntityDatabaseAdapterProvider } from '../IEntityDatabaseAdapterProvider'; import { EntityDataManager } from './EntityDataManager'; +import { EntityKnexDataManager } from './EntityKnexDataManager'; import { ReadThroughEntityCache } from './ReadThroughEntityCache'; import { IEntityMetricsAdapter } from '../metrics/IEntityMetricsAdapter'; @@ -22,6 +23,7 @@ export class EntityTableDataCoordinator< readonly databaseAdapter: EntityDatabaseAdapter; readonly cacheAdapter: IEntityCacheAdapter; readonly dataManager: EntityDataManager; + readonly knexDataManager: EntityKnexDataManager; constructor( readonly entityConfiguration: EntityConfiguration, @@ -40,6 +42,11 @@ export class EntityTableDataCoordinator< metricsAdapter, entityClassName, ); + this.knexDataManager = new EntityKnexDataManager( + this.databaseAdapter, + metricsAdapter, + entityClassName, + ); } getQueryContextProvider(): EntityQueryContextProvider { diff --git a/packages/entity/src/internal/__tests__/EntityDataManager-test.ts b/packages/entity/src/internal/__tests__/EntityDataManager-test.ts index b0978bc88..e6f3d5782 100644 --- a/packages/entity/src/internal/__tests__/EntityDataManager-test.ts +++ b/packages/entity/src/internal/__tests__/EntityDataManager-test.ts @@ -1,13 +1,11 @@ import { describe, expect, it, jest } from '@jest/globals'; import { anyNumber, - anyString, anything, deepEqual, instance, mock, resetCalls, - spy, verify, when, } from 'ts-mockito'; @@ -825,55 +823,6 @@ describe(EntityDataManager, () => { cacheSpy.mockReset(); }); - it('loads by field equality conjunction and does not cache', async () => { - const objects = getObjects(); - const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore( - testEntityConfiguration, - objects, - ); - const databaseAdapter = new StubDatabaseAdapter( - testEntityConfiguration, - dataStore, - ); - const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider(); - const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration); - const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter); - const entityDataManager = new EntityDataManager( - databaseAdapter, - entityCache, - new StubQueryContextProvider(), - new NoOpEntityMetricsAdapter(), - TestEntity.name, - ); - const queryContext = new StubQueryContextProvider().getQueryContext(); - - const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyByFieldEqualityConjunctionAsync'); - const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync'); - - const entityDatas = await entityDataManager.loadManyByFieldEqualityConjunctionAsync( - queryContext, - [ - { - fieldName: 'stringField', - fieldValue: 'hello', - }, - { - fieldName: 'intField', - fieldValue: 1, - }, - ], - {}, - ); - - expect(entityDatas).toHaveLength(2); - - expect(dbSpy).toHaveBeenCalled(); - expect(cacheSpy).not.toHaveBeenCalled(); - - dbSpy.mockReset(); - cacheSpy.mockReset(); - }); - it('handles DB errors as expected', async () => { const databaseAdapterMock = mock>(); when(databaseAdapterMock.fetchManyWhereAsync(anything(), anything(), anything())).thenReject( @@ -1054,56 +1003,6 @@ describe(EntityDataManager, () => { }), ), ).once(); - - resetCalls(metricsAdapterMock); - - await entityDataManager.loadManyByFieldEqualityConjunctionAsync( - queryContext, - [ - { - fieldName: 'testIndexedField', - fieldValue: 'unique1', - }, - ], - {}, - ); - verify( - metricsAdapterMock.logDataManagerLoadEvent( - deepEqual({ - type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, - isInTransaction: false, - entityClassName: TestEntity.name, - duration: anyNumber(), - count: 1, - }), - ), - ).once(); - - resetCalls(metricsAdapterMock); - - const databaseAdapterSpy = spy(databaseAdapter); - when( - databaseAdapterSpy.fetchManyByRawWhereClauseAsync( - anything(), - anyString(), - anything(), - anything(), - ), - ).thenResolve([]); - await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {}); - verify( - metricsAdapterMock.logDataManagerLoadEvent( - deepEqual({ - type: EntityMetricsLoadType.LOAD_MANY_RAW, - isInTransaction: false, - entityClassName: TestEntity.name, - duration: anyNumber(), - count: 0, - }), - ), - ).once(); - - verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never(); }); it('records metrics appropriately inside of transactions', async () => { @@ -1194,56 +1093,6 @@ describe(EntityDataManager, () => { }), ), ).once(); - - resetCalls(metricsAdapterMock); - - await entityDataManager.loadManyByFieldEqualityConjunctionAsync( - queryContext, - [ - { - fieldName: 'testIndexedField', - fieldValue: 'unique1', - }, - ], - {}, - ); - verify( - metricsAdapterMock.logDataManagerLoadEvent( - deepEqual({ - type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, - isInTransaction: true, - entityClassName: TestEntity.name, - duration: anyNumber(), - count: 1, - }), - ), - ).once(); - - resetCalls(metricsAdapterMock); - - const databaseAdapterSpy = spy(databaseAdapter); - when( - databaseAdapterSpy.fetchManyByRawWhereClauseAsync( - anything(), - anyString(), - anything(), - anything(), - ), - ).thenResolve([]); - await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {}); - verify( - metricsAdapterMock.logDataManagerLoadEvent( - deepEqual({ - type: EntityMetricsLoadType.LOAD_MANY_RAW, - isInTransaction: true, - entityClassName: TestEntity.name, - duration: anyNumber(), - count: 0, - }), - ), - ).once(); - - verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never(); }); }); }); diff --git a/packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts b/packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts new file mode 100644 index 000000000..2fe6d40aa --- /dev/null +++ b/packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts @@ -0,0 +1,241 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { + anyNumber, + anyString, + anything, + deepEqual, + instance, + mock, + resetCalls, + spy, + verify, + when, +} from 'ts-mockito'; + +import { EntityMetricsLoadType, IEntityMetricsAdapter } from '../../metrics/IEntityMetricsAdapter'; +import { NoOpEntityMetricsAdapter } from '../../metrics/NoOpEntityMetricsAdapter'; +import { StubDatabaseAdapter } from '../../utils/__testfixtures__/StubDatabaseAdapter'; +import { StubQueryContextProvider } from '../../utils/__testfixtures__/StubQueryContextProvider'; +import { + TestEntity, + testEntityConfiguration, + TestFields, +} from '../../utils/__testfixtures__/TestEntity'; +import { EntityKnexDataManager } from '../EntityKnexDataManager'; + +const getObjects = (): Map => + new Map([ + [ + testEntityConfiguration.tableName, + [ + { + customIdField: '1', + testIndexedField: 'unique1', + stringField: 'hello', + intField: 1, + dateField: new Date(), + nullableField: null, + }, + { + customIdField: '2', + testIndexedField: 'unique2', + stringField: 'hello', + intField: 1, + dateField: new Date(), + nullableField: null, + }, + { + customIdField: '3', + testIndexedField: 'unique3', + stringField: 'world', + intField: 1, + dateField: new Date(), + nullableField: null, + }, + ], + ], + ]); + +describe(EntityKnexDataManager, () => { + it('loads by field equality conjunction and does not cache', async () => { + const objects = getObjects(); + const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore( + testEntityConfiguration, + objects, + ); + const databaseAdapter = new StubDatabaseAdapter( + testEntityConfiguration, + dataStore, + ); + const entityDataManager = new EntityKnexDataManager( + databaseAdapter, + new NoOpEntityMetricsAdapter(), + TestEntity.name, + ); + const queryContext = new StubQueryContextProvider().getQueryContext(); + + const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyByFieldEqualityConjunctionAsync'); + + const entityDatas = await entityDataManager.loadManyByFieldEqualityConjunctionAsync( + queryContext, + [ + { + fieldName: 'stringField', + fieldValue: 'hello', + }, + { + fieldName: 'intField', + fieldValue: 1, + }, + ], + {}, + ); + + expect(entityDatas).toHaveLength(2); + + expect(dbSpy).toHaveBeenCalled(); + + dbSpy.mockReset(); + }); + + describe('metrics', () => { + it('records metrics appropriately outside of transactions', async () => { + const metricsAdapterMock = mock(); + const metricsAdapter = instance(metricsAdapterMock); + + const objects = getObjects(); + const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore( + testEntityConfiguration, + objects, + ); + const databaseAdapter = new StubDatabaseAdapter( + testEntityConfiguration, + dataStore, + ); + const entityDataManager = new EntityKnexDataManager( + databaseAdapter, + metricsAdapter, + TestEntity.name, + ); + const queryContext = new StubQueryContextProvider().getQueryContext(); + + await entityDataManager.loadManyByFieldEqualityConjunctionAsync( + queryContext, + [ + { + fieldName: 'testIndexedField', + fieldValue: 'unique1', + }, + ], + {}, + ); + verify( + metricsAdapterMock.logDataManagerLoadEvent( + deepEqual({ + type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, + isInTransaction: false, + entityClassName: TestEntity.name, + duration: anyNumber(), + count: 1, + }), + ), + ).once(); + + resetCalls(metricsAdapterMock); + + const databaseAdapterSpy = spy(databaseAdapter); + when( + databaseAdapterSpy.fetchManyByRawWhereClauseAsync( + anything(), + anyString(), + anything(), + anything(), + ), + ).thenResolve([]); + await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {}); + verify( + metricsAdapterMock.logDataManagerLoadEvent( + deepEqual({ + type: EntityMetricsLoadType.LOAD_MANY_RAW, + isInTransaction: false, + entityClassName: TestEntity.name, + duration: anyNumber(), + count: 0, + }), + ), + ).once(); + + verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never(); + }); + + it('records metrics appropriately inside of transactions', async () => { + const metricsAdapterMock = mock(); + const metricsAdapter = instance(metricsAdapterMock); + + const objects = getObjects(); + const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore( + testEntityConfiguration, + objects, + ); + const databaseAdapter = new StubDatabaseAdapter( + testEntityConfiguration, + dataStore, + ); + const entityDataManager = new EntityKnexDataManager( + databaseAdapter, + metricsAdapter, + TestEntity.name, + ); + + await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => { + await entityDataManager.loadManyByFieldEqualityConjunctionAsync( + queryContext, + [ + { + fieldName: 'testIndexedField', + fieldValue: 'unique1', + }, + ], + {}, + ); + verify( + metricsAdapterMock.logDataManagerLoadEvent( + deepEqual({ + type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, + isInTransaction: true, + entityClassName: TestEntity.name, + duration: anyNumber(), + count: 1, + }), + ), + ).once(); + + resetCalls(metricsAdapterMock); + + const databaseAdapterSpy = spy(databaseAdapter); + when( + databaseAdapterSpy.fetchManyByRawWhereClauseAsync( + anything(), + anyString(), + anything(), + anything(), + ), + ).thenResolve([]); + await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {}); + verify( + metricsAdapterMock.logDataManagerLoadEvent( + deepEqual({ + type: EntityMetricsLoadType.LOAD_MANY_RAW, + isInTransaction: true, + entityClassName: TestEntity.name, + duration: anyNumber(), + count: 0, + }), + ), + ).once(); + + verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never(); + }); + }); + }); +});