From c3f2f7229adb4fac07ddbaacca7df4719d1a6d94 Mon Sep 17 00:00:00 2001 From: Will Schurman Date: Sat, 31 Jan 2026 10:35:02 -0700 Subject: [PATCH] chore: Move knex-specific database adapter code out of EntityDatabaseAdapter --- .../src/PostgresEntityDatabaseAdapter.ts | 104 ------------ .../PostgresEntityDatabaseAdapterProvider.ts | 29 +++- .../src/PostgresEntityKnexDatabaseAdapter.ts | 124 +++++++++++++++ .../entity-database-adapter-knex/src/index.ts | 1 + packages/entity/src/EntityDatabaseAdapter.ts | 115 -------------- .../entity/src/EntityKnexDatabaseAdapter.ts | 147 +++++++++++++++++ .../src/IEntityDatabaseAdapterProvider.ts | 8 + ...izationResultBasedKnexEntityLoader-test.ts | 149 ++++++++++-------- .../__tests__/EntityDatabaseAdapter-test.ts | 53 +------ .../src/__tests__/EntityMutator-test.ts | 20 ++- packages/entity/src/index.ts | 1 + .../src/internal/EntityKnexDataManager.ts | 4 +- .../internal/EntityTableDataCoordinator.ts | 5 +- .../__tests__/EntityKnexDataManager-test.ts | 26 +-- .../__testfixtures__/StubDatabaseAdapter.ts | 104 +----------- .../StubDatabaseAdapterProvider.ts | 8 + .../StubKnexDatabaseAdapter.ts | 125 +++++++++++++++ 17 files changed, 562 insertions(+), 461 deletions(-) create mode 100644 packages/entity-database-adapter-knex/src/PostgresEntityKnexDatabaseAdapter.ts create mode 100644 packages/entity/src/EntityKnexDatabaseAdapter.ts create mode 100644 packages/entity/src/utils/__testfixtures__/StubKnexDatabaseAdapter.ts diff --git a/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapter.ts b/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapter.ts index 929de3c9c..7ffb3a9d3 100644 --- a/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapter.ts +++ b/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapter.ts @@ -2,10 +2,6 @@ import { EntityDatabaseAdapter, FieldTransformer, FieldTransformerMap, - TableFieldMultiValueEqualityCondition, - TableFieldSingleValueEqualityCondition, - TableQuerySelectionModifiers, - TableQuerySelectionModifiersWithOrderByRaw, } from '@expo/entity'; import { Knex } from 'knex'; @@ -86,106 +82,6 @@ export class PostgresEntityDatabaseAdapter< ); } - private applyQueryModifiersToQueryOrderByRaw( - query: Knex.QueryBuilder, - querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw, - ): Knex.QueryBuilder { - let ret = this.applyQueryModifiersToQuery(query, querySelectionModifiers); - - const { orderByRaw } = querySelectionModifiers; - if (orderByRaw !== undefined) { - ret = ret.orderByRaw(orderByRaw); - } - - return ret; - } - - private applyQueryModifiersToQuery( - query: Knex.QueryBuilder, - querySelectionModifiers: TableQuerySelectionModifiers, - ): Knex.QueryBuilder { - const { orderBy, offset, limit } = querySelectionModifiers; - - let ret = query; - - if (orderBy !== undefined) { - for (const orderBySpecification of orderBy) { - ret = ret.orderBy(orderBySpecification.columnName, orderBySpecification.order); - } - } - - if (offset !== undefined) { - ret = ret.offset(offset); - } - - if (limit !== undefined) { - ret = ret.limit(limit); - } - - return ret; - } - - protected async fetchManyByFieldEqualityConjunctionInternalAsync( - queryInterface: Knex, - tableName: string, - tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], - tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], - querySelectionModifiers: TableQuerySelectionModifiers, - ): Promise { - let query = queryInterface.select().from(tableName); - - if (tableFieldSingleValueEqualityOperands.length > 0) { - const whereObject: { [key: string]: any } = {}; - const nonNullTableFieldSingleValueEqualityOperands = - tableFieldSingleValueEqualityOperands.filter(({ tableValue }) => tableValue !== null); - const nullTableFieldSingleValueEqualityOperands = - tableFieldSingleValueEqualityOperands.filter(({ tableValue }) => tableValue === null); - - if (nonNullTableFieldSingleValueEqualityOperands.length > 0) { - for (const { tableField, tableValue } of nonNullTableFieldSingleValueEqualityOperands) { - whereObject[tableField] = tableValue; - } - query = query.where(whereObject); - } - if (nullTableFieldSingleValueEqualityOperands.length > 0) { - for (const { tableField } of nullTableFieldSingleValueEqualityOperands) { - query = query.whereNull(tableField); - } - } - } - - if (tableFieldMultiValueEqualityOperands.length > 0) { - for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) { - const nonNullTableValues = tableValues.filter((tableValue) => tableValue !== null); - query = query.where((builder) => { - builder.whereRaw('?? = ANY(?)', [tableField, [...nonNullTableValues]]); - // there was at least one null, allow null in this equality clause - if (nonNullTableValues.length !== tableValues.length) { - builder.orWhereNull(tableField); - } - }); - } - } - - query = this.applyQueryModifiersToQuery(query, querySelectionModifiers); - return await wrapNativePostgresCallAsync(() => query); - } - - protected async fetchManyByRawWhereClauseInternalAsync( - queryInterface: Knex, - tableName: string, - rawWhereClause: string, - bindings: object | any[], - querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw, - ): Promise { - let query = queryInterface - .select() - .from(tableName) - .whereRaw(rawWhereClause, bindings as any); - query = this.applyQueryModifiersToQueryOrderByRaw(query, querySelectionModifiers); - return await wrapNativePostgresCallAsync(() => query); - } - protected async insertInternalAsync( queryInterface: Knex, tableName: string, diff --git a/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts b/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts index 4d1b9bd0d..77060ce8f 100644 --- a/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts +++ b/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts @@ -1,15 +1,42 @@ import { EntityConfiguration, EntityDatabaseAdapter, + EntityKnexDatabaseAdapter, IEntityDatabaseAdapterProvider, } from '@expo/entity'; import { PostgresEntityDatabaseAdapter } from './PostgresEntityDatabaseAdapter'; +import { PostgresEntityKnexDatabaseAdapter } from './PostgresEntityKnexDatabaseAdapter'; export class PostgresEntityDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { + private readonly postgresEntityDatabaseAdapters = new Map< + EntityConfiguration, + PostgresEntityDatabaseAdapter + >(); + getDatabaseAdapter, TIDField extends keyof TFields>( entityConfiguration: EntityConfiguration, ): EntityDatabaseAdapter { - return new PostgresEntityDatabaseAdapter(entityConfiguration); + let adapter = this.postgresEntityDatabaseAdapters.get(entityConfiguration); + if (!adapter) { + adapter = new PostgresEntityDatabaseAdapter(entityConfiguration); + this.postgresEntityDatabaseAdapters.set(entityConfiguration, adapter); + } + return adapter as PostgresEntityDatabaseAdapter; + } + + getKnexDatabaseAdapter, TIDField extends keyof TFields>( + entityConfiguration: EntityConfiguration, + ): EntityKnexDatabaseAdapter { + // Get the regular adapter to access its field transformer map + const databaseAdapter = this.getDatabaseAdapter( + entityConfiguration, + ) as PostgresEntityDatabaseAdapter; + // Since getFieldTransformerMap is protected, we need to make it accessible + // For now, we'll create a new instance + return new PostgresEntityKnexDatabaseAdapter( + entityConfiguration, + (databaseAdapter as any).getFieldTransformerMap(), + ); } } diff --git a/packages/entity-database-adapter-knex/src/PostgresEntityKnexDatabaseAdapter.ts b/packages/entity-database-adapter-knex/src/PostgresEntityKnexDatabaseAdapter.ts new file mode 100644 index 000000000..a75446761 --- /dev/null +++ b/packages/entity-database-adapter-knex/src/PostgresEntityKnexDatabaseAdapter.ts @@ -0,0 +1,124 @@ +import { + EntityConfiguration, + EntityKnexDatabaseAdapter, + FieldTransformerMap, + TableFieldMultiValueEqualityCondition, + TableFieldSingleValueEqualityCondition, + TableQuerySelectionModifiers, + TableQuerySelectionModifiersWithOrderByRaw, +} from '@expo/entity'; +import { Knex } from 'knex'; + +import { wrapNativePostgresCallAsync } from './errors/wrapNativePostgresCallAsync'; + +export class PostgresEntityKnexDatabaseAdapter< + TFields extends Record, + TIDField extends keyof TFields, +> extends EntityKnexDatabaseAdapter { + constructor( + entityConfiguration: EntityConfiguration, + fieldTransformerMap: FieldTransformerMap, + ) { + super(entityConfiguration, fieldTransformerMap); + } + + private applyQueryModifiersToQueryOrderByRaw( + query: Knex.QueryBuilder, + querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw, + ): Knex.QueryBuilder { + let ret = this.applyQueryModifiersToQuery(query, querySelectionModifiers); + + const { orderByRaw } = querySelectionModifiers; + if (orderByRaw !== undefined) { + ret = ret.orderByRaw(orderByRaw); + } + + return ret; + } + + private applyQueryModifiersToQuery( + query: Knex.QueryBuilder, + querySelectionModifiers: TableQuerySelectionModifiers, + ): Knex.QueryBuilder { + const { orderBy, offset, limit } = querySelectionModifiers; + + let ret = query; + + if (orderBy !== undefined) { + for (const orderBySpecification of orderBy) { + ret = ret.orderBy(orderBySpecification.columnName, orderBySpecification.order); + } + } + + if (offset !== undefined) { + ret = ret.offset(offset); + } + + if (limit !== undefined) { + ret = ret.limit(limit); + } + + return ret; + } + + protected async fetchManyByFieldEqualityConjunctionInternalAsync( + queryInterface: Knex, + tableName: string, + tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], + tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], + querySelectionModifiers: TableQuerySelectionModifiers, + ): Promise { + let query = queryInterface.select().from(tableName); + + if (tableFieldSingleValueEqualityOperands.length > 0) { + const whereObject: { [key: string]: any } = {}; + const nonNullTableFieldSingleValueEqualityOperands = + tableFieldSingleValueEqualityOperands.filter(({ tableValue }) => tableValue !== null); + const nullTableFieldSingleValueEqualityOperands = + tableFieldSingleValueEqualityOperands.filter(({ tableValue }) => tableValue === null); + + if (nonNullTableFieldSingleValueEqualityOperands.length > 0) { + for (const { tableField, tableValue } of nonNullTableFieldSingleValueEqualityOperands) { + whereObject[tableField] = tableValue; + } + query = query.where(whereObject); + } + if (nullTableFieldSingleValueEqualityOperands.length > 0) { + for (const { tableField } of nullTableFieldSingleValueEqualityOperands) { + query = query.whereNull(tableField); + } + } + } + + if (tableFieldMultiValueEqualityOperands.length > 0) { + for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) { + const nonNullTableValues = tableValues.filter((tableValue) => tableValue !== null); + query = query.where((builder) => { + builder.whereRaw('?? = ANY(?)', [tableField, [...nonNullTableValues]]); + // there was at least one null, allow null in this equality clause + if (nonNullTableValues.length !== tableValues.length) { + builder.orWhereNull(tableField); + } + }); + } + } + + query = this.applyQueryModifiersToQuery(query, querySelectionModifiers); + return await wrapNativePostgresCallAsync(() => query); + } + + protected async fetchManyByRawWhereClauseInternalAsync( + queryInterface: Knex, + tableName: string, + rawWhereClause: string, + bindings: object | any[], + querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw, + ): Promise { + let query = queryInterface + .select() + .from(tableName) + .whereRaw(rawWhereClause, bindings as any); + query = this.applyQueryModifiersToQueryOrderByRaw(query, querySelectionModifiers); + return await wrapNativePostgresCallAsync(() => query); + } +} \ No newline at end of file diff --git a/packages/entity-database-adapter-knex/src/index.ts b/packages/entity-database-adapter-knex/src/index.ts index f4e3c95d6..f90f1383e 100644 --- a/packages/entity-database-adapter-knex/src/index.ts +++ b/packages/entity-database-adapter-knex/src/index.ts @@ -6,6 +6,7 @@ export * from './EntityFields'; export * from './PostgresEntityDatabaseAdapter'; +export * from './PostgresEntityKnexDatabaseAdapter'; export * from './PostgresEntityDatabaseAdapterProvider'; export * from './PostgresEntityQueryContextProvider'; export * from './errors/wrapNativePostgresCallAsync'; diff --git a/packages/entity/src/EntityDatabaseAdapter.ts b/packages/entity/src/EntityDatabaseAdapter.ts index ad181613d..d3f5ed013 100644 --- a/packages/entity/src/EntityDatabaseAdapter.ts +++ b/packages/entity/src/EntityDatabaseAdapter.ts @@ -221,92 +221,6 @@ export abstract class EntityDatabaseAdapter< tableTuples: (readonly any[])[], ): Promise; - /** - * Fetch many objects matching the conjunction of where clauses constructed from - * specified field equality operands. - * - * @param queryContext - query context with which to perform the fetch - * @param fieldEqualityOperands - list of field equality where clause operand specifications - * @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query - * @returns array of objects matching the query - */ - async fetchManyByFieldEqualityConjunctionAsync( - queryContext: EntityQueryContext, - fieldEqualityOperands: FieldEqualityCondition[], - querySelectionModifiers: QuerySelectionModifiers, - ): Promise[]> { - const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = []; - const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = []; - for (const operand of fieldEqualityOperands) { - if (isSingleValueFieldEqualityCondition(operand)) { - tableFieldSingleValueOperands.push({ - tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName), - tableValue: operand.fieldValue, - }); - } else { - tableFieldMultipleValueOperands.push({ - tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName), - tableValues: operand.fieldValues, - }); - } - } - - const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync( - queryContext.getQueryInterface(), - this.entityConfiguration.tableName, - tableFieldSingleValueOperands, - tableFieldMultipleValueOperands, - this.convertToTableQueryModifiers(querySelectionModifiers), - ); - - return results.map((result) => - transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result), - ); - } - - protected abstract fetchManyByFieldEqualityConjunctionInternalAsync( - queryInterface: any, - tableName: string, - tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], - tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], - querySelectionModifiers: TableQuerySelectionModifiers, - ): Promise; - - /** - * Fetch many objects matching the raw WHERE clause. - * - * @param queryContext - query context with which to perform the fetch - * @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, and orderBy for the query - * @returns array of objects matching the query - */ - async fetchManyByRawWhereClauseAsync( - queryContext: EntityQueryContext, - rawWhereClause: string, - bindings: any[] | object, - querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw, - ): Promise[]> { - const results = await this.fetchManyByRawWhereClauseInternalAsync( - queryContext.getQueryInterface(), - this.entityConfiguration.tableName, - rawWhereClause, - bindings, - this.convertToTableQueryModifiersWithOrderByRaw(querySelectionModifiers), - ); - - return results.map((result) => - transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result), - ); - } - - protected abstract fetchManyByRawWhereClauseInternalAsync( - queryInterface: any, - tableName: string, - rawWhereClause: string, - bindings: any[] | object, - querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw, - ): Promise; /** * Insert an object. @@ -446,33 +360,4 @@ export abstract class EntityDatabaseAdapter< tableIdField: string, id: any, ): Promise; - - private convertToTableQueryModifiersWithOrderByRaw( - querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw, - ): TableQuerySelectionModifiersWithOrderByRaw { - return { - ...this.convertToTableQueryModifiers(querySelectionModifiers), - orderByRaw: querySelectionModifiers.orderByRaw, - }; - } - - private convertToTableQueryModifiers( - querySelectionModifiers: QuerySelectionModifiers, - ): TableQuerySelectionModifiers { - const orderBy = querySelectionModifiers.orderBy; - return { - orderBy: - orderBy !== undefined - ? orderBy.map((orderBySpecification) => ({ - columnName: getDatabaseFieldForEntityField( - this.entityConfiguration, - orderBySpecification.fieldName, - ), - order: orderBySpecification.order, - })) - : undefined, - offset: querySelectionModifiers.offset, - limit: querySelectionModifiers.limit, - }; - } } diff --git a/packages/entity/src/EntityKnexDatabaseAdapter.ts b/packages/entity/src/EntityKnexDatabaseAdapter.ts new file mode 100644 index 000000000..9bec7a907 --- /dev/null +++ b/packages/entity/src/EntityKnexDatabaseAdapter.ts @@ -0,0 +1,147 @@ +import { EntityConfiguration } from './EntityConfiguration'; +import { + FieldEqualityCondition, + isSingleValueFieldEqualityCondition, + QuerySelectionModifiers, + QuerySelectionModifiersWithOrderByRaw, + TableFieldMultiValueEqualityCondition, + TableFieldSingleValueEqualityCondition, + TableQuerySelectionModifiers, + TableQuerySelectionModifiersWithOrderByRaw, +} from './EntityDatabaseAdapter'; +import { EntityQueryContext } from './EntityQueryContext'; +import { + FieldTransformerMap, + getDatabaseFieldForEntityField, + transformDatabaseObjectToFields, +} from './internal/EntityFieldTransformationUtils'; + +/** + * A database adapter that provides knex-specific query methods for fetching entities. + * These methods directly query the database without using DataLoader. + */ +export abstract class EntityKnexDatabaseAdapter< + TFields extends Record, + TIDField extends keyof TFields, +> { + constructor( + private readonly entityConfiguration: EntityConfiguration, + private readonly fieldTransformerMap: FieldTransformerMap, + ) {} + + /** + * Fetch many objects matching the conjunction of where clauses constructed from + * specified field equality operands. + * + * @param queryContext - query context with which to perform the fetch + * @param fieldEqualityOperands - list of field equality where clause operand specifications + * @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query + * @returns array of objects matching the query + */ + async fetchManyByFieldEqualityConjunctionAsync( + queryContext: EntityQueryContext, + fieldEqualityOperands: FieldEqualityCondition[], + querySelectionModifiers: QuerySelectionModifiers, + ): Promise[]> { + const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = []; + const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = []; + for (const operand of fieldEqualityOperands) { + if (isSingleValueFieldEqualityCondition(operand)) { + tableFieldSingleValueOperands.push({ + tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName), + tableValue: operand.fieldValue, + }); + } else { + tableFieldMultipleValueOperands.push({ + tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName), + tableValues: operand.fieldValues, + }); + } + } + + const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync( + queryContext.getQueryInterface(), + this.entityConfiguration.tableName, + tableFieldSingleValueOperands, + tableFieldMultipleValueOperands, + this.convertToTableQueryModifiers(querySelectionModifiers), + ); + + return results.map((result) => + transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result), + ); + } + + protected abstract fetchManyByFieldEqualityConjunctionInternalAsync( + queryInterface: any, + tableName: string, + tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], + tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], + querySelectionModifiers: TableQuerySelectionModifiers, + ): Promise; + + /** + * Fetch many objects matching the raw WHERE clause. + * + * @param queryContext - query context with which to perform the fetch + * @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, and orderBy for the query + * @returns array of objects matching the query + */ + async fetchManyByRawWhereClauseAsync( + queryContext: EntityQueryContext, + rawWhereClause: string, + bindings: any[] | object, + querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw, + ): Promise[]> { + const results = await this.fetchManyByRawWhereClauseInternalAsync( + queryContext.getQueryInterface(), + this.entityConfiguration.tableName, + rawWhereClause, + bindings, + this.convertToTableQueryModifiersWithOrderByRaw(querySelectionModifiers), + ); + + return results.map((result) => + transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result), + ); + } + + protected abstract fetchManyByRawWhereClauseInternalAsync( + queryInterface: any, + tableName: string, + rawWhereClause: string, + bindings: any[] | object, + querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw, + ): Promise; + + private convertToTableQueryModifiersWithOrderByRaw( + querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw, + ): TableQuerySelectionModifiersWithOrderByRaw { + return { + ...this.convertToTableQueryModifiers(querySelectionModifiers), + orderByRaw: querySelectionModifiers.orderByRaw, + }; + } + + private convertToTableQueryModifiers( + querySelectionModifiers: QuerySelectionModifiers, + ): TableQuerySelectionModifiers { + const orderBy = querySelectionModifiers.orderBy; + return { + orderBy: + orderBy !== undefined + ? orderBy.map((orderBySpecification) => ({ + columnName: getDatabaseFieldForEntityField( + this.entityConfiguration, + orderBySpecification.fieldName, + ), + order: orderBySpecification.order, + })) + : undefined, + offset: querySelectionModifiers.offset, + limit: querySelectionModifiers.limit, + }; + } +} \ No newline at end of file diff --git a/packages/entity/src/IEntityDatabaseAdapterProvider.ts b/packages/entity/src/IEntityDatabaseAdapterProvider.ts index 04c7e0aff..1bc4f84ba 100644 --- a/packages/entity/src/IEntityDatabaseAdapterProvider.ts +++ b/packages/entity/src/IEntityDatabaseAdapterProvider.ts @@ -2,6 +2,7 @@ import { EntityConfiguration } from './EntityConfiguration'; import { EntityDatabaseAdapter } from './EntityDatabaseAdapter'; +import { EntityKnexDatabaseAdapter } from './EntityKnexDatabaseAdapter'; /** * A database adapter provider vends database adapters for a particular database adapter type. @@ -14,6 +15,13 @@ export interface IEntityDatabaseAdapterProvider { getDatabaseAdapter, TIDField extends keyof TFields>( entityConfiguration: EntityConfiguration, ): EntityDatabaseAdapter; + + /** + * Vend a knex database adapter. + */ + getKnexDatabaseAdapter, TIDField extends keyof TFields>( + entityConfiguration: EntityConfiguration, + ): EntityKnexDatabaseAdapter; } /* c8 ignore stop - interface only */ diff --git a/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts b/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts index 2d4934ca9..475241d11 100644 --- a/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +++ b/packages/entity/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts @@ -14,6 +14,7 @@ import { ReadThroughEntityCache } from '../internal/ReadThroughEntityCache'; import { IEntityMetricsAdapter } from '../metrics/IEntityMetricsAdapter'; import { NoCacheStubCacheAdapterProvider } from '../utils/__testfixtures__/StubCacheAdapter'; import { StubDatabaseAdapter } from '../utils/__testfixtures__/StubDatabaseAdapter'; +import { StubKnexDatabaseAdapter } from '../utils/__testfixtures__/StubKnexDatabaseAdapter'; import { StubQueryContextProvider } from '../utils/__testfixtures__/StubQueryContextProvider'; import { TestEntity, @@ -44,42 +45,48 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { const id1 = uuidv4(); const id2 = uuidv4(); const id3 = uuidv4(); - const databaseAdapter = new StubDatabaseAdapter( + const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore( testEntityConfiguration, - StubDatabaseAdapter.convertFieldObjectsToDataStore( - testEntityConfiguration, - new Map([ + new Map([ + [ + testEntityConfiguration.tableName, [ - testEntityConfiguration.tableName, - [ - { - customIdField: id1, - stringField: 'huh', - intField: 4, - testIndexedField: '4', - dateField: new Date(), - nullableField: null, - }, - { - customIdField: id2, - stringField: 'huh', - intField: 4, - testIndexedField: '5', - dateField: new Date(), - nullableField: null, - }, - { - customIdField: id3, - stringField: 'huh2', - intField: 4, - testIndexedField: '6', - dateField: new Date(), - nullableField: null, - }, - ], + { + customIdField: id1, + stringField: 'huh', + intField: 4, + testIndexedField: '4', + dateField: new Date(), + nullableField: null, + }, + { + customIdField: id2, + stringField: 'huh', + intField: 4, + testIndexedField: '5', + dateField: new Date(), + nullableField: null, + }, + { + customIdField: id3, + stringField: 'huh2', + intField: 4, + testIndexedField: '6', + dateField: new Date(), + nullableField: null, + }, ], - ]), - ), + ], + ]), + ); + const databaseAdapter = new StubDatabaseAdapter( + testEntityConfiguration, + dataStore, + ); + const knexDatabaseAdapter = new StubKnexDatabaseAdapter( + testEntityConfiguration, + new Map(), // field transformer map + dataStore, ); const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider(); const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration); @@ -92,7 +99,7 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { TestEntity.name, ); const knexDataManager = new EntityKnexDataManager( - databaseAdapter, + knexDatabaseAdapter, metricsAdapter, TestEntity.name, ); @@ -164,42 +171,48 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { const id1 = uuidv4(); const id2 = uuidv4(); const id3 = uuidv4(); - const databaseAdapter = new StubDatabaseAdapter( + const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore( testEntityConfiguration, - StubDatabaseAdapter.convertFieldObjectsToDataStore( - testEntityConfiguration, - new Map([ + new Map([ + [ + testEntityConfiguration.tableName, [ - testEntityConfiguration.tableName, - [ - { - customIdField: id1, - stringField: 'huh', - intField: 4, - testIndexedField: '4', - dateField: new Date(), - nullableField: null, - }, - { - customIdField: id2, - stringField: 'huh', - intField: 4, - testIndexedField: '5', - dateField: new Date(), - nullableField: null, - }, - { - customIdField: id3, - stringField: 'huh2', - intField: 4, - testIndexedField: '6', - dateField: new Date(), - nullableField: null, - }, - ], + { + customIdField: id1, + stringField: 'huh', + intField: 4, + testIndexedField: '4', + dateField: new Date(), + nullableField: null, + }, + { + customIdField: id2, + stringField: 'huh', + intField: 4, + testIndexedField: '5', + dateField: new Date(), + nullableField: null, + }, + { + customIdField: id3, + stringField: 'huh2', + intField: 4, + testIndexedField: '6', + dateField: new Date(), + nullableField: null, + }, ], - ]), - ), + ], + ]), + ); + const databaseAdapter = new StubDatabaseAdapter( + testEntityConfiguration, + dataStore, + ); + const knexDatabaseAdapter = new StubKnexDatabaseAdapter( + testEntityConfiguration, + new Map(), // field transformer map + dataStore, ); const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider(); const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration); @@ -212,7 +225,7 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => { TestEntity.name, ); const knexDataManager = new EntityKnexDataManager( - databaseAdapter, + knexDatabaseAdapter, metricsAdapter, TestEntity.name, ); diff --git a/packages/entity/src/__tests__/EntityDatabaseAdapter-test.ts b/packages/entity/src/__tests__/EntityDatabaseAdapter-test.ts index 2762e9e15..cce940911 100644 --- a/packages/entity/src/__tests__/EntityDatabaseAdapter-test.ts +++ b/packages/entity/src/__tests__/EntityDatabaseAdapter-test.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import { instance, mock } from 'ts-mockito'; -import { - EntityDatabaseAdapter, - TableFieldMultiValueEqualityCondition, - TableFieldSingleValueEqualityCondition, -} from '../EntityDatabaseAdapter'; +import { EntityDatabaseAdapter } from '../EntityDatabaseAdapter'; import { EntityQueryContext } from '../EntityQueryContext'; import { EntityDatabaseAdapterEmptyInsertResultError, @@ -23,31 +19,23 @@ class TestEntityDatabaseAdapter extends EntityDatabaseAdapter { - return this.fetchRawWhereResults; - } - - protected async fetchManyByFieldEqualityConjunctionInternalAsync( - _queryInterface: any, - _tableName: string, - _tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], - _tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], - ): Promise { - return this.fetchEqualityConditionResults; - } - protected async insertInternalAsync( _queryInterface: any, _tableName: string, @@ -237,27 +207,6 @@ describe(EntityDatabaseAdapter, () => { }); }); - describe('fetchManyByFieldEqualityConjunction', () => { - it('transforms object', async () => { - const queryContext = instance(mock(EntityQueryContext)); - const adapter = new TestEntityDatabaseAdapter({ - fetchEqualityConditionResults: [{ string_field: 'hello' }], - }); - const results = await adapter.fetchManyByFieldEqualityConjunctionAsync(queryContext, [], {}); - expect(results).toEqual([{ stringField: 'hello' }]); - }); - }); - - describe('fetchManyWithRawWhereClause', () => { - it('transforms object', async () => { - const queryContext = instance(mock(EntityQueryContext)); - const adapter = new TestEntityDatabaseAdapter({ - fetchRawWhereResults: [{ string_field: 'hello' }], - }); - const results = await adapter.fetchManyByRawWhereClauseAsync(queryContext, 'hello', [], {}); - expect(results).toEqual([{ stringField: 'hello' }]); - }); - }); describe('insertAsync', () => { it('transforms object', async () => { diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index d8e5e9c43..b024b9c05 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -17,6 +17,7 @@ import { AuthorizationResultBasedEntityLoader } from '../AuthorizationResultBase import { EntityCompanionProvider } from '../EntityCompanionProvider'; import { EntityConfiguration } from '../EntityConfiguration'; import { EntityDatabaseAdapter } from '../EntityDatabaseAdapter'; +import { EntityKnexDatabaseAdapter } from '../EntityKnexDatabaseAdapter'; import { EntityLoaderFactory } from '../EntityLoaderFactory'; import { EntityLoaderUtils } from '../EntityLoaderUtils'; import { @@ -52,6 +53,7 @@ import { } from '../utils/__testfixtures__/SimpleTestEntity'; import { NoCacheStubCacheAdapterProvider } from '../utils/__testfixtures__/StubCacheAdapter'; import { StubDatabaseAdapter } from '../utils/__testfixtures__/StubDatabaseAdapter'; +import { StubKnexDatabaseAdapter } from '../utils/__testfixtures__/StubKnexDatabaseAdapter'; import { StubQueryContextProvider } from '../utils/__testfixtures__/StubQueryContextProvider'; import { TestEntity, @@ -362,12 +364,13 @@ const createEntityMutatorFactory = ( afterAll: [new TestMutationTrigger()], afterCommit: [new TestNonTransactionalMutationTrigger()], }; + const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore( + testEntityConfiguration, + new Map([[testEntityConfiguration.tableName, existingObjects]]), + ); const databaseAdapter = new StubDatabaseAdapter( testEntityConfiguration, - StubDatabaseAdapter.convertFieldObjectsToDataStore( - testEntityConfiguration, - new Map([[testEntityConfiguration.tableName, existingObjects]]), - ), + dataStore, ); const customStubDatabaseAdapterProvider: IEntityDatabaseAdapterProvider = { getDatabaseAdapter>( @@ -375,6 +378,15 @@ const createEntityMutatorFactory = ( ): EntityDatabaseAdapter { return databaseAdapter as any as EntityDatabaseAdapter; }, + getKnexDatabaseAdapter>( + entityConfiguration: EntityConfiguration, + ): EntityKnexDatabaseAdapter { + return new StubKnexDatabaseAdapter( + entityConfiguration, + new Map(), // field transformer map + dataStore, + ) as any; + }, }; const metricsAdapter = new NoOpEntityMetricsAdapter(); const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider(); diff --git a/packages/entity/src/index.ts b/packages/entity/src/index.ts index d939b4e9e..b47eaab91 100644 --- a/packages/entity/src/index.ts +++ b/packages/entity/src/index.ts @@ -23,6 +23,7 @@ export * from './EntityCompanionProvider'; export * from './EntityConfiguration'; export * from './EntityCreator'; export * from './EntityDatabaseAdapter'; +export * from './EntityKnexDatabaseAdapter'; export * from './EntityDeleter'; export * from './EntityFieldDefinition'; export * from './EntityFields'; diff --git a/packages/entity/src/internal/EntityKnexDataManager.ts b/packages/entity/src/internal/EntityKnexDataManager.ts index 3a18761cb..3b4b884a6 100644 --- a/packages/entity/src/internal/EntityKnexDataManager.ts +++ b/packages/entity/src/internal/EntityKnexDataManager.ts @@ -1,9 +1,9 @@ import { - EntityDatabaseAdapter, FieldEqualityCondition, QuerySelectionModifiers, QuerySelectionModifiersWithOrderByRaw, } from '../EntityDatabaseAdapter'; +import { EntityKnexDatabaseAdapter } from '../EntityKnexDatabaseAdapter'; import { EntityQueryContext } from '../EntityQueryContext'; import { timeAndLogLoadEventAsync } from '../metrics/EntityMetricsUtils'; import { EntityMetricsLoadType, IEntityMetricsAdapter } from '../metrics/IEntityMetricsAdapter'; @@ -19,7 +19,7 @@ export class EntityKnexDataManager< TIDField extends keyof TFields, > { constructor( - private readonly databaseAdapter: EntityDatabaseAdapter, + private readonly databaseAdapter: EntityKnexDatabaseAdapter, private readonly metricsAdapter: IEntityMetricsAdapter, private readonly entityClassName: string, ) {} diff --git a/packages/entity/src/internal/EntityTableDataCoordinator.ts b/packages/entity/src/internal/EntityTableDataCoordinator.ts index 5df415b72..46c62d77a 100644 --- a/packages/entity/src/internal/EntityTableDataCoordinator.ts +++ b/packages/entity/src/internal/EntityTableDataCoordinator.ts @@ -1,5 +1,6 @@ import { EntityConfiguration } from '../EntityConfiguration'; import { EntityDatabaseAdapter } from '../EntityDatabaseAdapter'; +import { EntityKnexDatabaseAdapter } from '../EntityKnexDatabaseAdapter'; import { EntityQueryContextProvider } from '../EntityQueryContextProvider'; import { IEntityCacheAdapter } from '../IEntityCacheAdapter'; import { IEntityCacheAdapterProvider } from '../IEntityCacheAdapterProvider'; @@ -21,6 +22,7 @@ export class EntityTableDataCoordinator< TIDField extends keyof TFields, > { readonly databaseAdapter: EntityDatabaseAdapter; + readonly knexDatabaseAdapter: EntityKnexDatabaseAdapter; readonly cacheAdapter: IEntityCacheAdapter; readonly dataManager: EntityDataManager; readonly knexDataManager: EntityKnexDataManager; @@ -34,6 +36,7 @@ export class EntityTableDataCoordinator< entityClassName: string, ) { this.databaseAdapter = databaseAdapterProvider.getDatabaseAdapter(entityConfiguration); + this.knexDatabaseAdapter = databaseAdapterProvider.getKnexDatabaseAdapter(entityConfiguration); this.cacheAdapter = cacheAdapterProvider.getCacheAdapter(entityConfiguration); this.dataManager = new EntityDataManager( this.databaseAdapter, @@ -43,7 +46,7 @@ export class EntityTableDataCoordinator< entityClassName, ); this.knexDataManager = new EntityKnexDataManager( - this.databaseAdapter, + this.knexDatabaseAdapter, metricsAdapter, entityClassName, ); diff --git a/packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts b/packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts index 2fe6d40aa..c31d22079 100644 --- a/packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts +++ b/packages/entity/src/internal/__tests__/EntityKnexDataManager-test.ts @@ -15,6 +15,7 @@ import { import { EntityMetricsLoadType, IEntityMetricsAdapter } from '../../metrics/IEntityMetricsAdapter'; import { NoOpEntityMetricsAdapter } from '../../metrics/NoOpEntityMetricsAdapter'; import { StubDatabaseAdapter } from '../../utils/__testfixtures__/StubDatabaseAdapter'; +import { StubKnexDatabaseAdapter } from '../../utils/__testfixtures__/StubKnexDatabaseAdapter'; import { StubQueryContextProvider } from '../../utils/__testfixtures__/StubQueryContextProvider'; import { TestEntity, @@ -63,18 +64,19 @@ describe(EntityKnexDataManager, () => { testEntityConfiguration, objects, ); - const databaseAdapter = new StubDatabaseAdapter( + const knexDatabaseAdapter = new StubKnexDatabaseAdapter( testEntityConfiguration, + new Map(), // field transformer map dataStore, ); const entityDataManager = new EntityKnexDataManager( - databaseAdapter, + knexDatabaseAdapter, new NoOpEntityMetricsAdapter(), TestEntity.name, ); const queryContext = new StubQueryContextProvider().getQueryContext(); - const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyByFieldEqualityConjunctionAsync'); + const dbSpy = jest.spyOn(knexDatabaseAdapter, 'fetchManyByFieldEqualityConjunctionAsync'); const entityDatas = await entityDataManager.loadManyByFieldEqualityConjunctionAsync( queryContext, @@ -108,12 +110,13 @@ describe(EntityKnexDataManager, () => { testEntityConfiguration, objects, ); - const databaseAdapter = new StubDatabaseAdapter( + const knexDatabaseAdapter = new StubKnexDatabaseAdapter( testEntityConfiguration, + new Map(), // field transformer map dataStore, ); const entityDataManager = new EntityKnexDataManager( - databaseAdapter, + knexDatabaseAdapter, metricsAdapter, TestEntity.name, ); @@ -143,9 +146,9 @@ describe(EntityKnexDataManager, () => { resetCalls(metricsAdapterMock); - const databaseAdapterSpy = spy(databaseAdapter); + const knexDatabaseAdapterSpy = spy(knexDatabaseAdapter); when( - databaseAdapterSpy.fetchManyByRawWhereClauseAsync( + knexDatabaseAdapterSpy.fetchManyByRawWhereClauseAsync( anything(), anyString(), anything(), @@ -177,12 +180,13 @@ describe(EntityKnexDataManager, () => { testEntityConfiguration, objects, ); - const databaseAdapter = new StubDatabaseAdapter( + const knexDatabaseAdapter = new StubKnexDatabaseAdapter( testEntityConfiguration, + new Map(), // field transformer map dataStore, ); const entityDataManager = new EntityKnexDataManager( - databaseAdapter, + knexDatabaseAdapter, metricsAdapter, TestEntity.name, ); @@ -212,9 +216,9 @@ describe(EntityKnexDataManager, () => { resetCalls(metricsAdapterMock); - const databaseAdapterSpy = spy(databaseAdapter); + const knexDatabaseAdapterSpy = spy(knexDatabaseAdapter); when( - databaseAdapterSpy.fetchManyByRawWhereClauseAsync( + knexDatabaseAdapterSpy.fetchManyByRawWhereClauseAsync( anything(), anyString(), anything(), diff --git a/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapter.ts b/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapter.ts index 029513e73..d271593f9 100644 --- a/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapter.ts +++ b/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapter.ts @@ -2,13 +2,7 @@ import invariant from 'invariant'; import { v7 as uuidv7 } from 'uuid'; import { EntityConfiguration } from '../../EntityConfiguration'; -import { - EntityDatabaseAdapter, - OrderByOrdering, - TableFieldMultiValueEqualityCondition, - TableFieldSingleValueEqualityCondition, - TableQuerySelectionModifiers, -} from '../../EntityDatabaseAdapter'; +import { EntityDatabaseAdapter } from '../../EntityDatabaseAdapter'; import { IntField, StringField } from '../../EntityFields'; import { FieldTransformerMap, @@ -84,102 +78,6 @@ export class StubDatabaseAdapter< return [...results]; } - private static compareByOrderBys( - orderBys: { - columnName: string; - order: OrderByOrdering; - }[], - objectA: { [key: string]: any }, - objectB: { [key: string]: any }, - ): 0 | 1 | -1 { - if (orderBys.length === 0) { - return 0; - } - - const currentOrderBy = orderBys[0]!; - const aField = objectA[currentOrderBy.columnName]; - const bField = objectB[currentOrderBy.columnName]; - switch (currentOrderBy.order) { - case OrderByOrdering.DESCENDING: { - // simulate NULLS FIRST for DESC - if (aField === null && bField === null) { - return 0; - } else if (aField === null) { - return -1; - } else if (bField === null) { - return 1; - } - - return aField > bField - ? -1 - : aField < bField - ? 1 - : this.compareByOrderBys(orderBys.slice(1), objectA, objectB); - } - case OrderByOrdering.ASCENDING: { - // simulate NULLS LAST for ASC - if (aField === null && bField === null) { - return 0; - } else if (bField === null) { - return -1; - } else if (aField === null) { - return 1; - } - - return bField > aField - ? -1 - : bField < aField - ? 1 - : this.compareByOrderBys(orderBys.slice(1), objectA, objectB); - } - } - } - - protected async fetchManyByFieldEqualityConjunctionInternalAsync( - _queryInterface: any, - tableName: string, - tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], - tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], - querySelectionModifiers: TableQuerySelectionModifiers, - ): Promise { - let filteredObjects = this.getObjectCollectionForTable(tableName); - for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) { - filteredObjects = filteredObjects.filter((obj) => obj[tableField] === tableValue); - } - - for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) { - filteredObjects = filteredObjects.filter((obj) => tableValues.includes(obj[tableField])); - } - - const orderBy = querySelectionModifiers.orderBy; - if (orderBy !== undefined) { - filteredObjects = filteredObjects.sort((a, b) => - StubDatabaseAdapter.compareByOrderBys(orderBy, a, b), - ); - } - - const offset = querySelectionModifiers.offset; - if (offset !== undefined) { - filteredObjects = filteredObjects.slice(offset); - } - - const limit = querySelectionModifiers.limit; - if (limit !== undefined) { - filteredObjects = filteredObjects.slice(0, 0 + limit); - } - - return filteredObjects; - } - - protected fetchManyByRawWhereClauseInternalAsync( - _queryInterface: any, - _tableName: string, - _rawWhereClause: string, - _bindings: object | any[], - _querySelectionModifiers: TableQuerySelectionModifiers, - ): Promise { - throw new Error('Raw WHERE clauses not supported for StubDatabaseAdapter'); - } private generateRandomID(): any { const idSchemaField = this.entityConfiguration2.schema.get(this.entityConfiguration2.idField); diff --git a/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts b/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts index 15c7a64e2..e9666e398 100644 --- a/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts +++ b/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts @@ -1,7 +1,9 @@ import { EntityConfiguration } from '../../EntityConfiguration'; import { EntityDatabaseAdapter } from '../../EntityDatabaseAdapter'; +import { EntityKnexDatabaseAdapter } from '../../EntityKnexDatabaseAdapter'; import { IEntityDatabaseAdapterProvider } from '../../IEntityDatabaseAdapterProvider'; import { StubDatabaseAdapter } from '../__testfixtures__/StubDatabaseAdapter'; +import { StubKnexDatabaseAdapter } from '../__testfixtures__/StubKnexDatabaseAdapter'; export class StubDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { private readonly objectCollection = new Map(); @@ -11,4 +13,10 @@ export class StubDatabaseAdapterProvider implements IEntityDatabaseAdapterProvid ): EntityDatabaseAdapter { return new StubDatabaseAdapter(entityConfiguration, this.objectCollection); } + + getKnexDatabaseAdapter, TIDField extends keyof TFields>( + entityConfiguration: EntityConfiguration, + ): EntityKnexDatabaseAdapter { + return new StubKnexDatabaseAdapter(entityConfiguration, new Map(), this.objectCollection); + } } diff --git a/packages/entity/src/utils/__testfixtures__/StubKnexDatabaseAdapter.ts b/packages/entity/src/utils/__testfixtures__/StubKnexDatabaseAdapter.ts new file mode 100644 index 000000000..137c136f2 --- /dev/null +++ b/packages/entity/src/utils/__testfixtures__/StubKnexDatabaseAdapter.ts @@ -0,0 +1,125 @@ +import { EntityConfiguration } from '../../EntityConfiguration'; +import { + OrderByOrdering, + TableFieldMultiValueEqualityCondition, + TableFieldSingleValueEqualityCondition, + TableQuerySelectionModifiers, + TableQuerySelectionModifiersWithOrderByRaw, +} from '../../EntityDatabaseAdapter'; +import { EntityKnexDatabaseAdapter } from '../../EntityKnexDatabaseAdapter'; +import { FieldTransformerMap } from '../../internal/EntityFieldTransformationUtils'; + +export class StubKnexDatabaseAdapter< + TFields extends Record, + TIDField extends keyof TFields, +> extends EntityKnexDatabaseAdapter { + constructor( + entityConfiguration: EntityConfiguration, + fieldTransformerMap: FieldTransformerMap, + private readonly dataStore: Map[]>, + ) { + super(entityConfiguration, fieldTransformerMap); + } + + private getObjectCollectionForTable(tableName: string): { [key: string]: any }[] { + const objects = this.dataStore.get(tableName); + return objects ? [...objects] : []; + } + + private static compareByOrderBys( + orderBys: { + columnName: string; + order: OrderByOrdering; + }[], + objectA: { [key: string]: any }, + objectB: { [key: string]: any }, + ): 0 | 1 | -1 { + if (orderBys.length === 0) { + return 0; + } + + const currentOrderBy = orderBys[0]!; + const aField = objectA[currentOrderBy.columnName]; + const bField = objectB[currentOrderBy.columnName]; + switch (currentOrderBy.order) { + case OrderByOrdering.DESCENDING: { + // simulate NULLS FIRST for DESC + if (aField === null && bField === null) { + return 0; + } else if (aField === null) { + return -1; + } else if (bField === null) { + return 1; + } + + return aField > bField + ? -1 + : aField < bField + ? 1 + : this.compareByOrderBys(orderBys.slice(1), objectA, objectB); + } + case OrderByOrdering.ASCENDING: { + // simulate NULLS LAST for ASC + if (aField === null && bField === null) { + return 0; + } else if (bField === null) { + return -1; + } else if (aField === null) { + return 1; + } + + return bField > aField + ? -1 + : bField < aField + ? 1 + : this.compareByOrderBys(orderBys.slice(1), objectA, objectB); + } + } + } + + protected async fetchManyByFieldEqualityConjunctionInternalAsync( + _queryInterface: any, + tableName: string, + tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], + tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], + querySelectionModifiers: TableQuerySelectionModifiers, + ): Promise { + let filteredObjects = this.getObjectCollectionForTable(tableName); + for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) { + filteredObjects = filteredObjects.filter((obj) => obj[tableField] === tableValue); + } + + for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) { + filteredObjects = filteredObjects.filter((obj) => tableValues.includes(obj[tableField])); + } + + const orderBy = querySelectionModifiers.orderBy; + if (orderBy !== undefined) { + filteredObjects = filteredObjects.sort((a, b) => + StubKnexDatabaseAdapter.compareByOrderBys(orderBy, a, b), + ); + } + + const offset = querySelectionModifiers.offset; + if (offset !== undefined) { + filteredObjects = filteredObjects.slice(offset); + } + + const limit = querySelectionModifiers.limit; + if (limit !== undefined) { + filteredObjects = filteredObjects.slice(0, 0 + limit); + } + + return filteredObjects; + } + + protected fetchManyByRawWhereClauseInternalAsync( + _queryInterface: any, + _tableName: string, + _rawWhereClause: string, + _bindings: object | any[], + _querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw, + ): Promise { + throw new Error('Raw WHERE clauses not supported for StubKnexDatabaseAdapter'); + } +} \ No newline at end of file