diff --git a/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test1.output.ts b/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test1.output.ts index c16f3d612..e88c54a50 100644 --- a/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test1.output.ts +++ b/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test1.output.ts @@ -2,10 +2,12 @@ import { ViewerContext } from '@expo/entity'; import { UserEntity } from './entities/UserEntity'; import { PostEntity } from './entities/PostEntity'; +import { knexLoader, knexLoaderWithAuthorizationResults } from "@expo/entity-database-adapter-knex"; + async function loadUser(viewerContext: ViewerContext) { // Basic loader calls - only transformed when using knex-specific methods const userLoader = UserEntity.loader(viewerContext); - const postLoader = PostEntity.knexLoader(viewerContext); + const postLoader = knexLoader(PostEntity, viewerContext); // These use knex-specific methods, so they should be transformed const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([ @@ -16,7 +18,7 @@ async function loadUser(viewerContext: ViewerContext) { ]); // Loader with authorization results - only transformed when using knex methods - const userLoaderWithAuth = UserEntity.knexLoaderWithAuthorizationResults(viewerContext); + const userLoaderWithAuth = knexLoaderWithAuthorizationResults(UserEntity, viewerContext); const rawResults = await userLoaderWithAuth.loadManyByRawWhereClauseAsync('age > ?', [18]); // Loader that doesn't use knex methods - should NOT be transformed diff --git a/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test2.output.ts b/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test2.output.ts index 4c61115d1..31dbd5523 100644 --- a/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test2.output.ts +++ b/packages/entity-codemod/src/transforms/__testfixtures__/v0.55.0-v0.56.0/test2.output.ts @@ -1,10 +1,12 @@ import { ViewerContext } from '@expo/entity'; import { CommentEntity } from './entities/CommentEntity'; +import { knexLoader, knexLoaderWithAuthorizationResults } from "@expo/entity-database-adapter-knex"; + // Chained calls const loadComments = async (viewerContext: ViewerContext) => { // Direct chaining with knex-specific method - const comments = await CommentEntity.knexLoader(viewerContext) + const comments = await knexLoader(CommentEntity, viewerContext) .loadManyByFieldEqualityConjunctionAsync([ { fieldName: 'postId', fieldValue: '123' } ]); @@ -15,8 +17,7 @@ const loadComments = async (viewerContext: ViewerContext) => { .loadByIDAsync('456'); // With authorization results and knex method - const commentsWithAuth = await CommentEntity - .knexLoaderWithAuthorizationResults(viewerContext) + const commentsWithAuth = await knexLoaderWithAuthorizationResults(CommentEntity, viewerContext) .loadManyByRawWhereClauseAsync('postId = ?', ['456']); // Edge cases - these should NOT be transformed diff --git a/packages/entity-codemod/src/transforms/v0.55.0-v0.56.0.ts b/packages/entity-codemod/src/transforms/v0.55.0-v0.56.0.ts index 92bf3f8cf..3e65e6480 100644 --- a/packages/entity-codemod/src/transforms/v0.55.0-v0.56.0.ts +++ b/packages/entity-codemod/src/transforms/v0.55.0-v0.56.0.ts @@ -79,7 +79,9 @@ function isKnexSpecificMethodUsed(j: API['jscodeshift'], node: any): boolean { return false; } -function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection): void { +function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection): boolean { + let transformed = false; + // Find all entity expressions of the form `Entity.loader(viewerContext)` root .find(j.CallExpression, { @@ -105,20 +107,28 @@ function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection, -): void { +): boolean { + let transformed = false; + // Find all entity expressions of the form `Entity.loaderWithAuthorizationResults(viewerContext)` root .find(j.CallExpression, { @@ -144,22 +154,88 @@ function transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationRes if (firstChar === firstChar?.toUpperCase()) { // Check if this loader uses knex-specific methods if (isKnexSpecificMethodUsed(j, path)) { - // Rename loaderWithAuthorizationResults to knexLoaderWithAuthorizationResults - if (loaderCallee.property.type === 'Identifier') { - loaderCallee.property.name = 'knexLoaderWithAuthorizationResults'; - } + // Transform Entity.loaderWithAuthorizationResults(viewerContext) → knexLoaderWithAuthorizationResults(Entity, viewerContext) + const entityIdentifier = loaderCallee.object; + const args = loaderCallExpression.arguments; + + j(path).replaceWith( + j.callExpression(j.identifier('knexLoaderWithAuthorizationResults'), [ + entityIdentifier, + ...args, + ]), + ); + transformed = true; } } } }); + + return transformed; +} + +function addKnexImportIfNeeded( + j: API['jscodeshift'], + root: Collection, + needsKnexLoader: boolean, + needsKnexLoaderWithAuthorizationResults: boolean, +): void { + if (!needsKnexLoader && !needsKnexLoaderWithAuthorizationResults) { + return; + } + + const specifiers: string[] = []; + if (needsKnexLoader) { + specifiers.push('knexLoader'); + } + if (needsKnexLoaderWithAuthorizationResults) { + specifiers.push('knexLoaderWithAuthorizationResults'); + } + + // Check if the import already exists + const existingImport = root.find(j.ImportDeclaration, { + source: { value: '@expo/entity-database-adapter-knex' }, + }); + + if (existingImport.size() > 0) { + // Add specifiers to existing import + const importDecl = existingImport.get(); + const existingSpecifierNames = new Set( + importDecl.node.specifiers?.map((s: any) => s.imported?.name).filter(Boolean) ?? [], + ); + + for (const specifier of specifiers) { + if (!existingSpecifierNames.has(specifier)) { + importDecl.node.specifiers?.push(j.importSpecifier(j.identifier(specifier))); + } + } + } else { + // Create new import declaration + const importSpecifiers = specifiers.map((s) => j.importSpecifier(j.identifier(s))); + const importDecl = j.importDeclaration( + importSpecifiers, + j.literal('@expo/entity-database-adapter-knex'), + ); + + // Add after the last import + const allImports = root.find(j.ImportDeclaration); + if (allImports.size() > 0) { + allImports.at(-1).insertAfter(importDecl); + } else { + // No imports, add at the top + root.get().node.program.body.unshift(importDecl); + } + } } export default function transformer(file: FileInfo, api: API, _options: Options): string { const j = api.jscodeshift; const root = j.withParser('ts')(file.source); - transformLoaderToKnexLoader(j, root); - transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(j, root); + const needsKnexLoader = transformLoaderToKnexLoader(j, root); + const needsKnexLoaderWithAuthorizationResults = + transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(j, root); + + addKnexImportIfNeeded(j, root, needsKnexLoader, needsKnexLoaderWithAuthorizationResults); return root.toSource(); } diff --git a/packages/entity-database-adapter-knex-testing-utils/src/StubPostgresDatabaseAdapterProvider.ts b/packages/entity-database-adapter-knex-testing-utils/src/StubPostgresDatabaseAdapterProvider.ts index 7b916a6aa..647887edc 100644 --- a/packages/entity-database-adapter-knex-testing-utils/src/StubPostgresDatabaseAdapterProvider.ts +++ b/packages/entity-database-adapter-knex-testing-utils/src/StubPostgresDatabaseAdapterProvider.ts @@ -3,37 +3,10 @@ import { EntityDatabaseAdapter, IEntityDatabaseAdapterProvider, } from '@expo/entity'; -import { - installEntityCompanionExtensions, - installEntityTableDataCoordinatorExtensions, - installReadonlyEntityExtensions, - installViewerScopedEntityCompanionExtensions, -} from '@expo/entity-database-adapter-knex'; import { StubPostgresDatabaseAdapter } from './StubPostgresDatabaseAdapter'; export class StubPostgresDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { - getExtensionsKey(): string { - return 'StubPostgresDatabaseAdapterProvider'; - } - - installExtensions({ - EntityCompanionClass, - EntityTableDataCoordinatorClass, - ViewerScopedEntityCompanionClass, - ReadonlyEntityClass, - }: { - EntityCompanionClass: typeof import('@expo/entity').EntityCompanion; - EntityTableDataCoordinatorClass: typeof import('@expo/entity').EntityTableDataCoordinator; - ViewerScopedEntityCompanionClass: typeof import('@expo/entity').ViewerScopedEntityCompanion; - ReadonlyEntityClass: typeof import('@expo/entity').ReadonlyEntity; - }): void { - installEntityCompanionExtensions({ EntityCompanionClass }); - installEntityTableDataCoordinatorExtensions({ EntityTableDataCoordinatorClass }); - installViewerScopedEntityCompanionExtensions({ ViewerScopedEntityCompanionClass }); - installReadonlyEntityExtensions({ ReadonlyEntityClass }); - } - private readonly objectCollection = new Map(); getDatabaseAdapter, TIDField extends keyof TFields>( diff --git a/packages/entity-database-adapter-knex/src/KnexEntityLoader.ts b/packages/entity-database-adapter-knex/src/KnexEntityLoader.ts deleted file mode 100644 index e77a9e7ed..000000000 --- a/packages/entity-database-adapter-knex/src/KnexEntityLoader.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - IEntityClass, - EntityPrivacyPolicy, - EntityQueryContext, - ReadonlyEntity, - ViewerContext, -} from '@expo/entity'; - -import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader'; -import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader'; - -/** - * The primary interface for loading entities via non-data-loader-based methods - * (knex queries). These methods are not batched or cached through dataloader. - */ -export class KnexEntityLoader< - TFields extends Record, - TIDField extends keyof NonNullable>, - TViewerContext extends ViewerContext, - TViewerContext2 extends TViewerContext, - TEntity extends ReadonlyEntity, - TPrivacyPolicy extends EntityPrivacyPolicy< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - TSelectedFields extends keyof TFields, -> { - constructor( - private readonly viewerContext: TViewerContext2, - private readonly queryContext: EntityQueryContext, - private readonly entityClass: IEntityClass< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - >, - ) {} - - /** - * Enforcing knex entity loader. All loads through this loader are - * guaranteed to be the values of successful results (or null for some loader methods), - * and will throw otherwise. - */ - enforcing(): EnforcingKnexEntityLoader< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return this.viewerContext - .getViewerScopedEntityCompanionForClass(this.entityClass) - .getKnexLoaderFactory() - .forLoadEnforcing(this.queryContext, { previousValue: null, cascadingDeleteCause: null }); - } - - /** - * Authorization-result-based knex entity loader. All loads through this - * loader are results (or null for some loader methods), where an unsuccessful result - * means an authorization error or entity construction error occurred. Other errors are thrown. - */ - withAuthorizationResults(): AuthorizationResultBasedKnexEntityLoader< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return this.viewerContext - .getViewerScopedEntityCompanionForClass(this.entityClass) - .getKnexLoaderFactory() - .forLoad(this.queryContext, { previousValue: null, cascadingDeleteCause: null }); - } -} diff --git a/packages/entity-database-adapter-knex/src/KnexEntityLoaderFactory.ts b/packages/entity-database-adapter-knex/src/KnexEntityLoaderFactory.ts index 8d0890a83..34856551d 100644 --- a/packages/entity-database-adapter-knex/src/KnexEntityLoaderFactory.ts +++ b/packages/entity-database-adapter-knex/src/KnexEntityLoaderFactory.ts @@ -1,5 +1,6 @@ import { EntityCompanion, + EntityConstructionUtils, EntityPrivacyPolicy, EntityPrivacyPolicyEvaluationContext, EntityQueryContext, @@ -7,7 +8,6 @@ import { ViewerContext, IEntityMetricsAdapter, } from '@expo/entity'; -import { EntityConstructionUtils } from '@expo/entity/src/EntityConstructionUtils'; import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader'; import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader'; diff --git a/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts b/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts index 8363a81ef..0c55aebe3 100644 --- a/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts +++ b/packages/entity-database-adapter-knex/src/PostgresEntityDatabaseAdapterProvider.ts @@ -5,10 +5,6 @@ import { } from '@expo/entity'; import { PostgresEntityDatabaseAdapter } from './PostgresEntityDatabaseAdapter'; -import { installEntityCompanionExtensions } from './extensions/EntityCompanionExtensions'; -import { installEntityTableDataCoordinatorExtensions } from './extensions/EntityTableDataCoordinatorExtensions'; -import { installReadonlyEntityExtensions } from './extensions/ReadonlyEntityExtensions'; -import { installViewerScopedEntityCompanionExtensions } from './extensions/ViewerScopedEntityCompanionExtensions'; export interface PostgresEntityDatabaseAdapterConfiguration { /** @@ -20,26 +16,6 @@ export interface PostgresEntityDatabaseAdapterConfiguration { export class PostgresEntityDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { constructor(private readonly configuration: PostgresEntityDatabaseAdapterConfiguration = {}) {} - getExtensionsKey(): string { - return 'PostgresEntityDatabaseAdapterProvider'; - } - - installExtensions({ - EntityCompanionClass, - EntityTableDataCoordinatorClass, - ViewerScopedEntityCompanionClass, - ReadonlyEntityClass, - }: { - EntityCompanionClass: typeof import('@expo/entity').EntityCompanion; - EntityTableDataCoordinatorClass: typeof import('@expo/entity').EntityTableDataCoordinator; - ViewerScopedEntityCompanionClass: typeof import('@expo/entity').ViewerScopedEntityCompanion; - ReadonlyEntityClass: typeof import('@expo/entity').ReadonlyEntity; - }): void { - installEntityCompanionExtensions({ EntityCompanionClass }); - installEntityTableDataCoordinatorExtensions({ EntityTableDataCoordinatorClass }); - installViewerScopedEntityCompanionExtensions({ ViewerScopedEntityCompanionClass }); - installReadonlyEntityExtensions({ ReadonlyEntityClass }); - } getDatabaseAdapter, TIDField extends keyof TFields>( entityConfiguration: EntityConfiguration, diff --git a/packages/entity-database-adapter-knex/src/ViewerScopedKnexEntityLoaderFactory.ts b/packages/entity-database-adapter-knex/src/ViewerScopedKnexEntityLoaderFactory.ts deleted file mode 100644 index 3e47162f5..000000000 --- a/packages/entity-database-adapter-knex/src/ViewerScopedKnexEntityLoaderFactory.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - EntityPrivacyPolicy, - EntityPrivacyPolicyEvaluationContext, - EntityQueryContext, - ReadonlyEntity, - ViewerContext, -} from '@expo/entity'; - -import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader'; -import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader'; -import { KnexEntityLoaderFactory } from './KnexEntityLoaderFactory'; - -/** - * Provides a cleaner API for loading entities via knex by passing through the ViewerContext. - */ -export class ViewerScopedKnexEntityLoaderFactory< - TFields extends Record, - TIDField extends keyof NonNullable>, - TViewerContext extends ViewerContext, - TEntity extends ReadonlyEntity, - TPrivacyPolicy extends EntityPrivacyPolicy< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - TSelectedFields extends keyof TFields, -> { - constructor( - private readonly knexEntityLoaderFactory: KnexEntityLoaderFactory< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - >, - private readonly viewerContext: TViewerContext, - ) {} - - forLoad( - queryContext: EntityQueryContext, - privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - ): AuthorizationResultBasedKnexEntityLoader< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return this.knexEntityLoaderFactory.forLoad( - this.viewerContext, - queryContext, - privacyPolicyEvaluationContext, - ); - } - - forLoadEnforcing( - queryContext: EntityQueryContext, - privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - ): EnforcingKnexEntityLoader< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return this.knexEntityLoaderFactory.forLoadEnforcing( - this.viewerContext, - queryContext, - privacyPolicyEvaluationContext, - ); - } -} diff --git a/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts b/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts index 62ff0487e..f6a576601 100644 --- a/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts +++ b/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts @@ -17,6 +17,7 @@ import { PostgresTestEntity } from '../__testfixtures__/PostgresTestEntity'; import { PostgresTriggerTestEntity } from '../__testfixtures__/PostgresTriggerTestEntity'; import { PostgresValidatorTestEntity } from '../__testfixtures__/PostgresValidatorTestEntity'; import { createKnexIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createKnexIntegrationTestEntityCompanionProvider'; +import { knexLoader, knexLoaderWithAuthorizationResults } from '../knexLoader'; describe('postgres entity integration', () => { let knexInstance: Knex; @@ -429,7 +430,7 @@ describe('postgres entity integration', () => { ); // Test basic SQL query with parameters - const catOwners = await PostgresTestEntity.knexLoader(vc1) + const catOwners = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`has_a_cat = ${true}`) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -439,7 +440,7 @@ describe('postgres entity integration', () => { expect(catOwners[1]!.getField('name')).toBe('Charlie'); // Test with limit and offset - const limitedResults = await PostgresTestEntity.knexLoader(vc1) + const limitedResults = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`has_a_cat = ${true}`) .orderBy('name', OrderByOrdering.ASCENDING) .limit(1) @@ -479,7 +480,7 @@ describe('postgres entity integration', () => { ); // Test AND condition - const bothPets = await PostgresTestEntity.knexLoader(vc1) + const bothPets = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(and(eq('has_a_cat', true), eq('has_a_dog', true))) .executeAsync(); @@ -487,7 +488,7 @@ describe('postgres entity integration', () => { expect(bothPets[0]!.getField('name')).toBe('User3'); // Test OR condition - const eitherPet = await PostgresTestEntity.knexLoader(vc1) + const eitherPet = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(or(eq('has_a_cat', false), eq('has_a_dog', false))) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -497,7 +498,7 @@ describe('postgres entity integration', () => { expect(eitherPet[1]!.getField('name')).toBe('User2'); // Test IN array - const specificUsers = await PostgresTestEntity.knexLoader(vc1) + const specificUsers = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(inArray('name', ['User1', 'User3'])) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -507,7 +508,7 @@ describe('postgres entity integration', () => { expect(specificUsers[1]!.getField('name')).toBe('User3'); // Test complex condition - const complexQuery = await PostgresTestEntity.knexLoader(vc1) + const complexQuery = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(and(or(eq('has_a_cat', true), eq('has_a_dog', true)), neq('name', 'User2'))) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -534,7 +535,7 @@ describe('postgres entity integration', () => { .createAsync(), ); - const firstCatOwnerLimit1 = await PostgresTestEntity.knexLoader(vc1) + const firstCatOwnerLimit1 = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`has_a_cat = ${true}`) .orderBy('name', OrderByOrdering.ASCENDING) .limit(1) @@ -544,7 +545,7 @@ describe('postgres entity integration', () => { expect(firstCatOwnerLimit1[0]?.getField('name')).toBe('First'); // Test executeFirstAsync with no results - const noDogOwnerLimit1 = await PostgresTestEntity.knexLoader(vc1) + const noDogOwnerLimit1 = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`has_a_dog = ${true}`) .limit(1) .executeAsync(); @@ -570,7 +571,7 @@ describe('postgres entity integration', () => { ); // Test with authorization results - const results = await PostgresTestEntity.knexLoaderWithAuthorizationResults(vc1) + const results = await knexLoaderWithAuthorizationResults(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'AuthTest%'}`) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -586,7 +587,7 @@ describe('postgres entity integration', () => { expect(results[1]!.value.getField('name')).toBe('AuthTest2'); } - const firstResultLimit1 = await PostgresTestEntity.knexLoaderWithAuthorizationResults(vc1) + const firstResultLimit1 = await knexLoaderWithAuthorizationResults(PostgresTestEntity, vc1) .loadManyBySQL(sql`has_a_cat = ${false}`) .limit(1) .executeAsync(); @@ -626,7 +627,7 @@ describe('postgres entity integration', () => { // Test raw SQL for dynamic column names with orderBySQL const sortColumn = 'name'; - const rawResults = await PostgresTestEntity.knexLoader(vc1) + const rawResults = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`${raw('name')} LIKE ${'RawTest%'}`) .orderBySQL(sql`${raw(sortColumn)} DESC`) .executeAsync(); @@ -637,7 +638,7 @@ describe('postgres entity integration', () => { expect(rawResults[2]!.getField('name')).toBe('RawTest1'); // Test complex ORDER BY with CASE statement - const priorityResults = await PostgresTestEntity.knexLoader(vc1) + const priorityResults = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'RawTest%'}`) .orderBySQL( sql`CASE @@ -654,7 +655,7 @@ describe('postgres entity integration', () => { expect(priorityResults[2]!.getField('name')).toBe('RawTest2'); // has dog only // Test raw SQL with complex expressions - using CASE statement - const complexExpression = await PostgresTestEntity.knexLoader(vc1) + const complexExpression = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL( sql`${raw('CASE WHEN has_a_cat THEN 1 ELSE 0 END')} + ${raw( 'CASE WHEN has_a_dog THEN 1 ELSE 0 END', @@ -688,7 +689,7 @@ describe('postgres entity integration', () => { sql`name = ${'JoinTest1'}`, sql`(has_a_cat = ${true} AND has_a_dog = ${true})`, ]; - const joinedResults = await PostgresTestEntity.knexLoader(vc1) + const joinedResults = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(SQLFragment.join(conditions, ' OR ')) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -758,7 +759,7 @@ describe('postgres entity integration', () => { ); // Test 1: Simple orderBySQL with raw column - const simpleOrder = await PostgresTestEntity.knexLoader(vc1) + const simpleOrder = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`${raw('name')} DESC`) .executeAsync(); @@ -774,7 +775,7 @@ describe('postgres entity integration', () => { const priority2 = 2; const priority3 = 3; const priority4 = 4; - const caseOrder = await PostgresTestEntity.knexLoader(vc1) + const caseOrder = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL( sql`CASE @@ -793,7 +794,7 @@ describe('postgres entity integration', () => { expect(caseOrder[3]!.getField('name')).toBe('OrderTest4'); // Neither = 4 // Test 3: Order by array length (PostgreSQL specific) - const arrayLengthOrder = await PostgresTestEntity.knexLoader(vc1) + const arrayLengthOrder = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`COALESCE(array_length(string_array, 1), 0) DESC, ${raw('name')} ASC`) .executeAsync(); @@ -805,7 +806,7 @@ describe('postgres entity integration', () => { expect(arrayLengthOrder[3]!.getField('name')).toBe('OrderTest3'); // null = 0 // Test 4: Multiple orderBySQL calls (last one wins) - const multipleOrderBy = await PostgresTestEntity.knexLoader(vc1) + const multipleOrderBy = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`${raw('name')} DESC`) // This will be overridden .orderBySQL(sql`${raw('name')} ASC`) // This one wins @@ -816,7 +817,7 @@ describe('postgres entity integration', () => { expect(multipleOrderBy[3]!.getField('name')).toBe('OrderTest4'); // Test 5: Combining orderBySQL with limit and offset - const limitedOrder = await PostgresTestEntity.knexLoader(vc1) + const limitedOrder = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`${raw('name')} ASC`) .limit(2) @@ -828,7 +829,7 @@ describe('postgres entity integration', () => { expect(limitedOrder[1]!.getField('name')).toBe('OrderTest3'); // Test 6: orderBySQL with NULLS FIRST/LAST - const nullsOrder = await PostgresTestEntity.knexLoader(vc1) + const nullsOrder = await knexLoader(PostgresTestEntity, vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`string_array IS NULL, ${raw('name')} ASC`) .executeAsync(); @@ -848,7 +849,7 @@ describe('postgres entity integration', () => { .createAsync(); // Create a query builder - const queryBuilder = PostgresTestEntity.knexLoader(vc1).loadManyBySQL( + const queryBuilder = knexLoader(PostgresTestEntity, vc1).loadManyBySQL( sql`name = ${'MultiExecTest'}`, ); @@ -868,7 +869,7 @@ describe('postgres entity integration', () => { ); // A new query builder should work fine - const newQueryBuilder = PostgresTestEntity.knexLoader(vc1).loadManyBySQL( + const newQueryBuilder = knexLoader(PostgresTestEntity, vc1).loadManyBySQL( sql`name = ${'MultiExecTest'}`, ); @@ -906,7 +907,8 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await PostgresTestEntity.knexLoader( + const results = await knexLoader( + PostgresTestEntity, vc1, ).loadManyByFieldEqualityConjunctionAsync([ { @@ -921,7 +923,8 @@ describe('postgres entity integration', () => { expect(results).toHaveLength(2); - const results2 = await PostgresTestEntity.knexLoader( + const results2 = await knexLoader( + PostgresTestEntity, vc1, ).loadManyByFieldEqualityConjunctionAsync([ { fieldName: 'hasADog', fieldValues: [true, false] }, @@ -944,7 +947,8 @@ describe('postgres entity integration', () => { PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'c').createAsync(), ); - const results = await PostgresTestEntity.knexLoader( + const results = await knexLoader( + PostgresTestEntity, vc1, ).loadManyByFieldEqualityConjunctionAsync([], { limit: 2, @@ -987,13 +991,15 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await PostgresTestEntity.knexLoader( + const results = await knexLoader( + PostgresTestEntity, vc1, ).loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: null }]); expect(results).toHaveLength(2); expect(results[0]!.getField('name')).toBeNull(); - const results2 = await PostgresTestEntity.knexLoader( + const results2 = await knexLoader( + PostgresTestEntity, vc1, ).loadManyByFieldEqualityConjunctionAsync( [ @@ -1025,7 +1031,7 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync( + const results = await knexLoader(PostgresTestEntity, vc1).loadManyByRawWhereClauseAsync( 'name = ?', ['hello'], ); @@ -1044,7 +1050,7 @@ describe('postgres entity integration', () => { ); await expect( - PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync('invalid_column = ?', [ + knexLoader(PostgresTestEntity, vc1).loadManyByRawWhereClauseAsync('invalid_column = ?', [ 'hello', ]), ).rejects.toThrow(); @@ -1074,7 +1080,7 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync( + const results = await knexLoader(PostgresTestEntity, vc1).loadManyByRawWhereClauseAsync( 'has_a_dog = ?', [true], { @@ -1092,7 +1098,8 @@ describe('postgres entity integration', () => { expect(results).toHaveLength(2); expect(results.map((e) => e.getField('name'))).toEqual(['b', 'c']); - const resultsMultipleOrderBy = await PostgresTestEntity.knexLoader( + const resultsMultipleOrderBy = await knexLoader( + PostgresTestEntity, vc1, ).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], { orderBy: [ @@ -1110,7 +1117,8 @@ describe('postgres entity integration', () => { expect(resultsMultipleOrderBy).toHaveLength(3); expect(resultsMultipleOrderBy.map((e) => e.getField('name'))).toEqual(['c', 'b', 'a']); - const resultsOrderByRaw = await PostgresTestEntity.knexLoader( + const resultsOrderByRaw = await knexLoader( + PostgresTestEntity, vc1, ).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], { orderByRaw: 'has_a_dog ASC, name DESC', @@ -1386,7 +1394,7 @@ describe('postgres entity integration', () => { ); // Get first page - const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1402,7 +1410,7 @@ describe('postgres entity integration', () => { expect(firstPage.pageInfo.hasPreviousPage).toBe(false); // Get second page using cursor - const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, after: firstPage.pageInfo.endCursor!, pagination: { @@ -1425,7 +1433,7 @@ describe('postgres entity integration', () => { ); // Get last page - const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const lastPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1441,7 +1449,7 @@ describe('postgres entity integration', () => { expect(lastPage.pageInfo.hasPreviousPage).toBe(true); // Get previous page using cursor - const previousPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const previousPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 3, before: lastPage.pageInfo.startCursor!, pagination: { @@ -1464,7 +1472,7 @@ describe('postgres entity integration', () => { ); // Query only entities with cats - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 2, where: sql`has_a_cat = ${true}`, pagination: { @@ -1481,7 +1489,7 @@ describe('postgres entity integration', () => { expect(page.pageInfo.hasNextPage).toBe(true); // Get next page with same where condition - const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const nextPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 2, after: page.pageInfo.endCursor!, where: sql`has_a_cat = ${true}`, @@ -1504,7 +1512,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 4, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1533,7 +1541,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, where: sql`name = ${'NonexistentName'}`, pagination: { @@ -1554,7 +1562,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1568,7 +1576,7 @@ describe('postgres entity integration', () => { expect(page.edges[2]?.cursor).toBeTruthy(); // Start from middle item - const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const nextPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 2, after: page.edges[1]!.cursor, pagination: { @@ -1587,7 +1595,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1598,7 +1606,7 @@ describe('postgres entity integration', () => { expect(page.edges).toHaveLength(3); // Navigate using cursor - const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const nextPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, after: page.pageInfo.endCursor!, pagination: { @@ -1631,7 +1639,7 @@ describe('postgres entity integration', () => { // Test backward pagination with DESCENDING order // This internally flips DESCENDING to ASCENDING for the query - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1650,7 +1658,7 @@ describe('postgres entity integration', () => { expect(page.pageInfo.hasNextPage).toBe(false); // Verify the order is maintained correctly with forward pagination too - const forwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const forwardPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1686,7 +1694,7 @@ describe('postgres entity integration', () => { } // Pagination with only name in orderBy - ID should be added automatically for stability - const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1697,7 +1705,7 @@ describe('postgres entity integration', () => { expect(firstPage.edges).toHaveLength(3); // Get second page - const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, after: firstPage.pageInfo.endCursor!, pagination: { @@ -1715,7 +1723,7 @@ describe('postgres entity integration', () => { expect(intersection).toHaveLength(0); // Test with explicit ID in orderBy (shouldn't duplicate) - const pageWithExplicitId = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const pageWithExplicitId = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1733,7 +1741,7 @@ describe('postgres entity integration', () => { // Try with completely invalid cursor await expect( - PostgresTestEntity.knexLoader(vc).loadPageAsync({ + knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, after: 'not-a-valid-cursor', pagination: { @@ -1746,7 +1754,7 @@ describe('postgres entity integration', () => { // Try with valid base64 but invalid JSON const invalidJsonCursor = Buffer.from('not json').toString('base64url'); await expect( - PostgresTestEntity.knexLoader(vc).loadPageAsync({ + knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, after: invalidJsonCursor, pagination: { @@ -1761,7 +1769,7 @@ describe('postgres entity integration', () => { 'base64url', ); await expect( - PostgresTestEntity.knexLoader(vc).loadPageAsync({ + knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, after: missingFieldsCursor, pagination: { @@ -1787,7 +1795,7 @@ describe('postgres entity integration', () => { } // Test with enforcing loader (standard pagination) - const pageEnforced = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const pageEnforced = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 4, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1803,7 +1811,7 @@ describe('postgres entity integration', () => { expect(pageEnforced.edges[3]?.node.getField('name')).toBe('David'); // Test pagination continues correctly - const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 4, after: pageEnforced.pageInfo.endCursor!, pagination: { @@ -1819,7 +1827,8 @@ describe('postgres entity integration', () => { // Test with authorization result-based loader // Note: Currently loadPageWithSearchAsync with knexLoaderWithAuthorizationResults // returns entities directly, not Result objects (unlike loadManyBySQL) - const pageWithAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( + const pageWithAuth = await knexLoaderWithAuthorizationResults( + PostgresTestEntity, vc, ).loadPageAsync({ first: 3, @@ -1850,7 +1859,7 @@ describe('postgres entity integration', () => { } // Load with limit 5 - should have hasNextPage=true - const page1 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page1 = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 5, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1862,7 +1871,7 @@ describe('postgres entity integration', () => { expect(page1.pageInfo.hasNextPage).toBe(true); // Load the last entity - const page2 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page2 = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 5, after: page1.pageInfo.endCursor!, pagination: { @@ -1891,7 +1900,7 @@ describe('postgres entity integration', () => { // Test that orderByFragment overrides orderBy completely // orderBy would sort by name ascending (Alice, Bob, Charlie, David) // orderByFragment will sort by name descending (David, Charlie, Bob, Alice) - const results = await PostgresTestEntity.knexLoader(vc) + const results = await knexLoader(PostgresTestEntity, vc) .loadManyBySQL(sql`1 = 1`, { orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }], orderByFragment: sql`name DESC`, @@ -1935,7 +1944,7 @@ describe('postgres entity integration', () => { } // Test 1: Regular loader with ILIKE search - const iLikeSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const iLikeSearchRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 2, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -1950,7 +1959,8 @@ describe('postgres entity integration', () => { expect(iLikeSearchRegular.pageInfo.hasNextPage).toBe(true); // Test 2: Authorization result loader with same ILIKE search - const iLikeSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( + const iLikeSearchAuth = await knexLoaderWithAuthorizationResults( + PostgresTestEntity, vc, ).loadPageAsync({ first: 2, @@ -1968,7 +1978,7 @@ describe('postgres entity integration', () => { expect(iLikeSearchAuth.pageInfo.hasNextPage).toBe(true); // Test 3: Regular loader with TRIGRAM search - const trigramSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const trigramSearchRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -1986,7 +1996,8 @@ describe('postgres entity integration', () => { expect(foundNames).toContain('Frank Johnson'); // Test 4: Authorization result loader with TRIGRAM search - const trigramSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( + const trigramSearchAuth = await knexLoaderWithAuthorizationResults( + PostgresTestEntity, vc, ).loadPageAsync({ first: 3, @@ -2005,7 +2016,7 @@ describe('postgres entity integration', () => { expect(foundNamesAuth).toContain('Frank Johnson'); // Test 5: Test pagination with cursor for both loader types - const firstPageRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPageRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 1, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2017,7 +2028,7 @@ describe('postgres entity integration', () => { expect(firstPageRegular.edges).toHaveLength(1); expect(firstPageRegular.edges[0]?.node.getField('name')).toBe('Bob Smith'); - const secondPageRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPageRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 1, after: firstPageRegular.pageInfo.endCursor!, pagination: { @@ -2031,7 +2042,7 @@ describe('postgres entity integration', () => { expect(secondPageRegular.edges[0]?.node.getField('name')).toBe('David Smith'); // Test 6: Combine search with WHERE filter for both loaders - const filteredSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const filteredSearchRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, where: sql`has_a_cat = ${true}`, pagination: { @@ -2048,7 +2059,8 @@ describe('postgres entity integration', () => { expect(filteredSearchRegular.edges[1]?.node.getField('name')).toBe('Charlie Johnson'); expect(filteredSearchRegular.edges[1]?.node.getField('hasACat')).toBe(true); - const filteredSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( + const filteredSearchAuth = await knexLoaderWithAuthorizationResults( + PostgresTestEntity, vc, ).loadPageAsync({ first: 10, @@ -2065,7 +2077,7 @@ describe('postgres entity integration', () => { expect(filteredSearchAuth.edges[1]?.node.getField('name')).toBe('Charlie Johnson'); // Test 7: Test with both loader types - const withRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const withRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 1, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2076,7 +2088,8 @@ describe('postgres entity integration', () => { expect(withRegular.edges).toHaveLength(1); - const withAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( + const withAuth = await knexLoaderWithAuthorizationResults( + PostgresTestEntity, vc, ).loadPageAsync({ first: 1, @@ -2117,7 +2130,7 @@ describe('postgres entity integration', () => { } // Search for names containing "Johnson" - const searchResults = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const searchResults = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2131,7 +2144,7 @@ describe('postgres entity integration', () => { expect(searchResults.edges[1]?.node.getField('name')).toBe('Eve Johnson'); // Search for names containing "Smith" with pagination - const smithPage1 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const smithPage1 = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 1, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2145,7 +2158,7 @@ describe('postgres entity integration', () => { expect(smithPage1.pageInfo.hasNextPage).toBe(true); // Get next page - const smithPage2 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const smithPage2 = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 1, after: smithPage1.pageInfo.endCursor!, pagination: { @@ -2160,7 +2173,7 @@ describe('postgres entity integration', () => { expect(smithPage2.pageInfo.hasNextPage).toBe(false); // Test partial match (case insensitive) - const partialMatch = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const partialMatch = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2174,7 +2187,7 @@ describe('postgres entity integration', () => { expect(partialMatch.edges[1]?.node.getField('name')).toBe('Eve Johnson'); // Test search with WHERE clause - const combinedFilter = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const combinedFilter = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, where: sql`has_a_cat = ${true}`, pagination: { @@ -2206,7 +2219,7 @@ describe('postgres entity integration', () => { } // Forward pagination with ILIKE search - const forwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const forwardPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 2, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2223,7 +2236,7 @@ describe('postgres entity integration', () => { }); // Backward pagination with ILIKE search - const backwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const backwardPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 2, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2244,7 +2257,7 @@ describe('postgres entity integration', () => { let hasNext = true; while (hasNext) { - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, ...(cursor && { after: cursor }), pagination: { @@ -2303,7 +2316,7 @@ describe('postgres entity integration', () => { const pageSize = 3; while (true) { - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: pageSize, ...(cursor && { after: cursor }), pagination: { @@ -2344,7 +2357,7 @@ describe('postgres entity integration', () => { pageCount = 0; while (true) { - const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: pageSize, ...(backCursor && { before: backCursor }), pagination: { @@ -2404,7 +2417,7 @@ describe('postgres entity integration', () => { } // Search for similar names to "Johnson" using trigram - const trigramSearch = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const trigramSearch = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2425,7 +2438,7 @@ describe('postgres entity integration', () => { expect(foundNames).toContain('Johnsen'); // Test combining with WHERE clause - const filteredTrigram = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const filteredTrigram = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, where: sql`has_a_cat = ${true}`, pagination: { @@ -2475,7 +2488,7 @@ describe('postgres entity integration', () => { } // First page with trigram search (no cursor) - const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2494,7 +2507,7 @@ describe('postgres entity integration', () => { // Second page with cursor // Note: For trigram search with cursor, we use regular orderBy instead of custom order // so results might not be in perfect similarity order, but should still be filtered - const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, after: firstPageCursor!, pagination: { @@ -2512,7 +2525,7 @@ describe('postgres entity integration', () => { // being passed through the parallel query path // Test backward pagination with cursor - const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const lastPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 2, before: firstPageCursor!, pagination: { @@ -2529,7 +2542,7 @@ describe('postgres entity integration', () => { // Test with WHERE clause, cursor, and search const firstEdgeCursor = firstPage.edges[0]?.cursor; expect(firstEdgeCursor).toBeDefined(); - const filteredWithCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const filteredWithCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 2, after: firstEdgeCursor!, where: sql`has_a_cat = ${true}`, @@ -2580,7 +2593,7 @@ describe('postgres entity integration', () => { } // Test 1: Forward pagination (first) - const firstPageForward = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPageForward = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 4, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2601,7 +2614,7 @@ describe('postgres entity integration', () => { expect(forwardNames).not.toContain('Williams'); // Test 2: Backward pagination (last) - const lastPageBackward = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const lastPageBackward = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 4, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2623,7 +2636,7 @@ describe('postgres entity integration', () => { // Test 3: Test cursor pagination with trigram search // With the improved implementation, TRIGRAM cursor pagination now preserves // similarity-based ordering by computing similarity scores dynamically via subquery - const firstPageForwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPageForwardCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2642,7 +2655,7 @@ describe('postgres entity integration', () => { })); const firstPageForwardCursorIDs = firstPageForwardCursorData.map((d) => d.id); - const secondPageForwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPageForwardCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, after: firstPageForwardCursor.pageInfo.endCursor!, pagination: { @@ -2670,7 +2683,7 @@ describe('postgres entity integration', () => { expect(overlapForwardCursor).toHaveLength(0); // Test 4: test backward cursor pagination with trigram search - const firstPageBackwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPageBackwardCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2689,7 +2702,7 @@ describe('postgres entity integration', () => { const firstPageBackwardIDs = firstPageBackwardCursorData.map((d) => d.id); expect(firstPageBackwardIDs.length).toBeGreaterThan(0); - const secondPageBackwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPageBackwardCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 3, before: firstPageBackwardCursor.pageInfo.startCursor!, pagination: { @@ -2747,7 +2760,7 @@ describe('postgres entity integration', () => { } // Test TRIGRAM search with extraOrderByFields for stable pagination - const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2766,7 +2779,7 @@ describe('postgres entity integration', () => { // Get second page using cursor // With extraOrderByFields, cursor includes hasACat field which provides more stable pagination - const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 3, after: firstPageCursor!, pagination: { @@ -2787,7 +2800,7 @@ describe('postgres entity integration', () => { expect(overlap).toHaveLength(0); // Test backward pagination with extraOrderByFields - const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const lastPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ last: 2, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2802,7 +2815,7 @@ describe('postgres entity integration', () => { // Test that extraOrderByFields provides consistent ordering // Get all results in one go for comparison - const allResultsPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + const allResultsPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, diff --git a/packages/entity-database-adapter-knex/src/__tests__/ReadonlyEntity-test.ts b/packages/entity-database-adapter-knex/src/__tests__/ReadonlyEntity-test.ts index e3543538a..6d6e1e5ac 100644 --- a/packages/entity-database-adapter-knex/src/__tests__/ReadonlyEntity-test.ts +++ b/packages/entity-database-adapter-knex/src/__tests__/ReadonlyEntity-test.ts @@ -1,17 +1,18 @@ -import { ReadonlyEntity, ViewerContext } from '@expo/entity'; +import { ViewerContext } from '@expo/entity'; import { describe, expect, it } from '@jest/globals'; import { AuthorizationResultBasedKnexEntityLoader } from '../AuthorizationResultBasedKnexEntityLoader'; import { EnforcingKnexEntityLoader } from '../EnforcingKnexEntityLoader'; +import { knexLoader, knexLoaderWithAuthorizationResults } from '../knexLoader'; import { TestEntity } from './fixtures/TestEntity'; import { createUnitTestPostgresEntityCompanionProvider } from './fixtures/createUnitTestPostgresEntityCompanionProvider'; -describe(ReadonlyEntity, () => { +describe('knexLoader', () => { describe('knexLoader', () => { it('creates a new EnforcingKnexEntityLoader', async () => { const companionProvider = createUnitTestPostgresEntityCompanionProvider(); const viewerContext = new ViewerContext(companionProvider); - expect(TestEntity.knexLoader(viewerContext)).toBeInstanceOf(EnforcingKnexEntityLoader); + expect(knexLoader(TestEntity, viewerContext)).toBeInstanceOf(EnforcingKnexEntityLoader); }); }); @@ -19,7 +20,7 @@ describe(ReadonlyEntity, () => { it('creates a new AuthorizationResultBasedKnexEntityLoader', async () => { const companionProvider = createUnitTestPostgresEntityCompanionProvider(); const viewerContext = new ViewerContext(companionProvider); - expect(TestEntity.knexLoaderWithAuthorizationResults(viewerContext)).toBeInstanceOf( + expect(knexLoaderWithAuthorizationResults(TestEntity, viewerContext)).toBeInstanceOf( AuthorizationResultBasedKnexEntityLoader, ); }); diff --git a/packages/entity-database-adapter-knex/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts b/packages/entity-database-adapter-knex/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts index 1ad0525e8..647887edc 100644 --- a/packages/entity-database-adapter-knex/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +++ b/packages/entity-database-adapter-knex/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts @@ -5,33 +5,8 @@ import { } from '@expo/entity'; import { StubPostgresDatabaseAdapter } from './StubPostgresDatabaseAdapter'; -import { installEntityCompanionExtensions } from '../../extensions/EntityCompanionExtensions'; -import { installEntityTableDataCoordinatorExtensions } from '../../extensions/EntityTableDataCoordinatorExtensions'; -import { installReadonlyEntityExtensions } from '../../extensions/ReadonlyEntityExtensions'; -import { installViewerScopedEntityCompanionExtensions } from '../../extensions/ViewerScopedEntityCompanionExtensions'; export class StubPostgresDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { - getExtensionsKey(): string { - return 'StubPostgresDatabaseAdapterProvider'; - } - - installExtensions({ - EntityCompanionClass, - EntityTableDataCoordinatorClass, - ViewerScopedEntityCompanionClass, - ReadonlyEntityClass, - }: { - EntityCompanionClass: typeof import('@expo/entity').EntityCompanion; - EntityTableDataCoordinatorClass: typeof import('@expo/entity').EntityTableDataCoordinator; - ViewerScopedEntityCompanionClass: typeof import('@expo/entity').ViewerScopedEntityCompanion; - ReadonlyEntityClass: typeof import('@expo/entity').ReadonlyEntity; - }): void { - installEntityCompanionExtensions({ EntityCompanionClass }); - installEntityTableDataCoordinatorExtensions({ EntityTableDataCoordinatorClass }); - installViewerScopedEntityCompanionExtensions({ ViewerScopedEntityCompanionClass }); - installReadonlyEntityExtensions({ ReadonlyEntityClass }); - } - private readonly objectCollection = new Map(); getDatabaseAdapter, TIDField extends keyof TFields>( diff --git a/packages/entity-database-adapter-knex/src/extensions/EntityCompanionExtensions.ts b/packages/entity-database-adapter-knex/src/extensions/EntityCompanionExtensions.ts deleted file mode 100644 index 2be80331f..000000000 --- a/packages/entity-database-adapter-knex/src/extensions/EntityCompanionExtensions.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { EntityCompanion, EntityPrivacyPolicy, ReadonlyEntity, ViewerContext } from '@expo/entity'; - -import { KnexEntityLoaderFactory } from '../KnexEntityLoaderFactory'; - -const KNEX_LOADER_FACTORY = Symbol('knexLoaderFactory'); - -declare module '@expo/entity' { - interface EntityCompanion< - TFields extends Record, - TIDField extends keyof NonNullable>, - TViewerContext extends ViewerContext, - TEntity extends ReadonlyEntity, - TPrivacyPolicy extends EntityPrivacyPolicy< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - TSelectedFields extends keyof TFields, - > { - [KNEX_LOADER_FACTORY]: - | KnexEntityLoaderFactory< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > - | undefined; - - getKnexLoaderFactory(): KnexEntityLoaderFactory< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - >; - } -} - -export function installEntityCompanionExtensions({ - EntityCompanionClass, -}: { - EntityCompanionClass: typeof EntityCompanion; -}): void { - EntityCompanionClass.prototype.getKnexLoaderFactory = function < - TFields extends Record, - TIDField extends keyof NonNullable>, - TViewerContext extends ViewerContext, - TEntity extends ReadonlyEntity, - TPrivacyPolicy extends EntityPrivacyPolicy< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - TSelectedFields extends keyof TFields, - >( - this: EntityCompanion< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - >, - ): KnexEntityLoaderFactory< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return (this[KNEX_LOADER_FACTORY] ??= new KnexEntityLoaderFactory( - this, - this.tableDataCoordinator.getKnexDataManager(), - this.metricsAdapter, - )); - }; -} diff --git a/packages/entity-database-adapter-knex/src/extensions/EntityTableDataCoordinatorExtensions.ts b/packages/entity-database-adapter-knex/src/extensions/EntityTableDataCoordinatorExtensions.ts deleted file mode 100644 index 155da9bbd..000000000 --- a/packages/entity-database-adapter-knex/src/extensions/EntityTableDataCoordinatorExtensions.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { EntityDatabaseAdapter, EntityTableDataCoordinator } from '@expo/entity'; -import assert from 'assert'; - -import { BasePostgresEntityDatabaseAdapter } from '../BasePostgresEntityDatabaseAdapter'; -import { EntityKnexDataManager } from '../internal/EntityKnexDataManager'; - -const KNEX_DATA_MANAGER = Symbol('knexDataManager'); - -declare module '@expo/entity' { - interface EntityTableDataCoordinator< - TFields extends Record, - TIDField extends keyof TFields, - > { - [KNEX_DATA_MANAGER]: EntityKnexDataManager | undefined; - getKnexDataManager(): EntityKnexDataManager; - } -} - -function requireBasePostgresAdapter< - TFields extends Record, - TIDField extends keyof TFields, ->( - databaseAdapter: EntityDatabaseAdapter, -): BasePostgresEntityDatabaseAdapter { - assert( - databaseAdapter instanceof BasePostgresEntityDatabaseAdapter, - `Cannot create KnexDataManager for EntityTableDataCoordinator with non-Postgres database adapter.`, - ); - return databaseAdapter; -} - -export function installEntityTableDataCoordinatorExtensions({ - EntityTableDataCoordinatorClass, -}: { - EntityTableDataCoordinatorClass: typeof EntityTableDataCoordinator; -}): void { - EntityTableDataCoordinatorClass.prototype.getKnexDataManager = function < - TFields extends Record, - TIDField extends keyof TFields, - >(this: EntityTableDataCoordinator): EntityKnexDataManager { - return (this[KNEX_DATA_MANAGER] ??= new EntityKnexDataManager( - this.entityConfiguration, - requireBasePostgresAdapter(this.databaseAdapter), - this.metricsAdapter, - this.entityClassName, - )); - }; -} diff --git a/packages/entity-database-adapter-knex/src/extensions/ReadonlyEntityExtensions.ts b/packages/entity-database-adapter-knex/src/extensions/ReadonlyEntityExtensions.ts deleted file mode 100644 index ff9569a5e..000000000 --- a/packages/entity-database-adapter-knex/src/extensions/ReadonlyEntityExtensions.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { - EntityPrivacyPolicy, - EntityQueryContext, - IEntityClass, - ReadonlyEntity, - ViewerContext, -} from '@expo/entity'; - -import { AuthorizationResultBasedKnexEntityLoader } from '../AuthorizationResultBasedKnexEntityLoader'; -import { EnforcingKnexEntityLoader } from '../EnforcingKnexEntityLoader'; -import { KnexEntityLoader } from '../KnexEntityLoader'; - -declare module '@expo/entity' { - namespace ReadonlyEntity { - export function knexLoader< - TMFields extends object, - TMIDField extends keyof NonNullable>, - TMViewerContext extends ViewerContext, - TMViewerContext2 extends TMViewerContext, - TMEntity extends ReadonlyEntity, - TMPrivacyPolicy extends EntityPrivacyPolicy< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMSelectedFields - >, - TMSelectedFields extends keyof TMFields = keyof TMFields, - >( - this: IEntityClass< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - >, - viewerContext: TMViewerContext2, - queryContext?: EntityQueryContext, - ): EnforcingKnexEntityLoader< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - >; - - export function knexLoaderWithAuthorizationResults< - TMFields extends object, - TMIDField extends keyof NonNullable>, - TMViewerContext extends ViewerContext, - TMViewerContext2 extends TMViewerContext, - TMEntity extends ReadonlyEntity, - TMPrivacyPolicy extends EntityPrivacyPolicy< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMSelectedFields - >, - TMSelectedFields extends keyof TMFields = keyof TMFields, - >( - this: IEntityClass< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - >, - viewerContext: TMViewerContext2, - queryContext?: EntityQueryContext, - ): AuthorizationResultBasedKnexEntityLoader< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - >; - } -} - -class ReadonlyEntityExtensions { - /** - * Vend knex loader for loading entities via non-data-loader methods in a given query context. - * @param viewerContext - viewer context of loading user - * @param queryContext - query context in which to perform the load - */ - static knexLoader< - TMFields extends object, - TMIDField extends keyof NonNullable>, - TMViewerContext extends ViewerContext, - TMViewerContext2 extends TMViewerContext, - TMEntity extends ReadonlyEntity, - TMPrivacyPolicy extends EntityPrivacyPolicy< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMSelectedFields - >, - TMSelectedFields extends keyof TMFields = keyof TMFields, - >( - this: IEntityClass< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - >, - viewerContext: TMViewerContext2, - queryContext: EntityQueryContext = viewerContext - .getViewerScopedEntityCompanionForClass(this) - .getQueryContextProvider() - .getQueryContext(), - ): EnforcingKnexEntityLoader< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - > { - return new KnexEntityLoader(viewerContext, queryContext, this).enforcing(); - } - - /** - * Vend knex loader for loading entities via non-data-loader methods in a given query context. - * @param viewerContext - viewer context of loading user - * @param queryContext - query context in which to perform the load - */ - static knexLoaderWithAuthorizationResults< - TMFields extends object, - TMIDField extends keyof NonNullable>, - TMViewerContext extends ViewerContext, - TMViewerContext2 extends TMViewerContext, - TMEntity extends ReadonlyEntity, - TMPrivacyPolicy extends EntityPrivacyPolicy< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMSelectedFields - >, - TMSelectedFields extends keyof TMFields = keyof TMFields, - >( - this: IEntityClass< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - >, - viewerContext: TMViewerContext2, - queryContext: EntityQueryContext = viewerContext - .getViewerScopedEntityCompanionForClass(this) - .getQueryContextProvider() - .getQueryContext(), - ): AuthorizationResultBasedKnexEntityLoader< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - > { - return new KnexEntityLoader(viewerContext, queryContext, this).withAuthorizationResults(); - } -} - -export function installReadonlyEntityExtensions({ - ReadonlyEntityClass, -}: { - ReadonlyEntityClass: typeof ReadonlyEntity; -}): void { - ReadonlyEntityClass.knexLoader = ReadonlyEntityExtensions.knexLoader; - ReadonlyEntityClass.knexLoaderWithAuthorizationResults = - ReadonlyEntityExtensions.knexLoaderWithAuthorizationResults; -} diff --git a/packages/entity-database-adapter-knex/src/extensions/ViewerScopedEntityCompanionExtensions.ts b/packages/entity-database-adapter-knex/src/extensions/ViewerScopedEntityCompanionExtensions.ts deleted file mode 100644 index 9fa988bf6..000000000 --- a/packages/entity-database-adapter-knex/src/extensions/ViewerScopedEntityCompanionExtensions.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - EntityPrivacyPolicy, - ReadonlyEntity, - ViewerContext, - ViewerScopedEntityCompanion, -} from '@expo/entity'; - -import { ViewerScopedKnexEntityLoaderFactory } from '../ViewerScopedKnexEntityLoaderFactory'; - -declare module '@expo/entity' { - interface ViewerScopedEntityCompanion< - TFields extends Record, - TIDField extends keyof NonNullable>, - TViewerContext extends ViewerContext, - TEntity extends ReadonlyEntity, - TPrivacyPolicy extends EntityPrivacyPolicy< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - TSelectedFields extends keyof TFields, - > { - getKnexLoaderFactory(): ViewerScopedKnexEntityLoaderFactory< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - >; - } -} - -export function installViewerScopedEntityCompanionExtensions({ - ViewerScopedEntityCompanionClass, -}: { - ViewerScopedEntityCompanionClass: typeof ViewerScopedEntityCompanion; -}): void { - ViewerScopedEntityCompanionClass.prototype.getKnexLoaderFactory = function < - TFields extends Record, - TIDField extends keyof NonNullable>, - TViewerContext extends ViewerContext, - TEntity extends ReadonlyEntity, - TPrivacyPolicy extends EntityPrivacyPolicy< - TFields, - TIDField, - TViewerContext, - TEntity, - TSelectedFields - >, - TSelectedFields extends keyof TFields, - >( - this: ViewerScopedEntityCompanion< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - >, - ): ViewerScopedKnexEntityLoaderFactory< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return new ViewerScopedKnexEntityLoaderFactory( - this.entityCompanion.getKnexLoaderFactory(), - this.viewerContext, - ); - }; -} diff --git a/packages/entity-database-adapter-knex/src/index.ts b/packages/entity-database-adapter-knex/src/index.ts index df336fddc..4693e1101 100644 --- a/packages/entity-database-adapter-knex/src/index.ts +++ b/packages/entity-database-adapter-knex/src/index.ts @@ -9,17 +9,15 @@ export * from './BasePostgresEntityDatabaseAdapter'; export * from './BaseSQLQueryBuilder'; export * from './EnforcingKnexEntityLoader'; export * from './EntityFields'; -export * from './KnexEntityLoader'; export * from './KnexEntityLoaderFactory'; +export * from './knexLoader'; export * from './PaginationStrategy'; export * from './PostgresEntityDatabaseAdapter'; export * from './PostgresEntityDatabaseAdapterProvider'; export * from './PostgresEntityQueryContextProvider'; export * from './SQLOperator'; -export * from './ViewerScopedKnexEntityLoaderFactory'; export * from './errors/wrapNativePostgresCallAsync'; -export * from './extensions/EntityCompanionExtensions'; -export * from './extensions/EntityTableDataCoordinatorExtensions'; -export * from './extensions/ReadonlyEntityExtensions'; -export * from './extensions/ViewerScopedEntityCompanionExtensions'; export * from './internal/EntityKnexDataManager'; +export * from './internal/getKnexDataManager'; +export * from './internal/getKnexEntityLoaderFactory'; +export * from './internal/weakMaps'; diff --git a/packages/entity-database-adapter-knex/src/internal/__tests__/weakMaps-test.ts b/packages/entity-database-adapter-knex/src/internal/__tests__/weakMaps-test.ts new file mode 100644 index 000000000..dcc3da9aa --- /dev/null +++ b/packages/entity-database-adapter-knex/src/internal/__tests__/weakMaps-test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from '@jest/globals'; + +import { computeIfAbsentInWeakMap } from '../weakMaps'; + +describe(computeIfAbsentInWeakMap, () => { + it('computes a value when absent', () => { + const map = new WeakMap(); + const key = {}; + const blah = computeIfAbsentInWeakMap(map, key, () => 'world'); + expect(blah).toEqual(map.get(key)); + }); + + it('does not compute a value when already present', () => { + const map = new WeakMap(); + const key = {}; + map.set(key, 'world'); + let didCompute = false; + const blah = computeIfAbsentInWeakMap(map, key, () => { + didCompute = true; + return 'world2'; + }); + expect(blah).toEqual(map.get(key)); + expect(didCompute).toBe(false); + }); +}); diff --git a/packages/entity-database-adapter-knex/src/internal/getKnexDataManager.ts b/packages/entity-database-adapter-knex/src/internal/getKnexDataManager.ts new file mode 100644 index 000000000..a1e1fac73 --- /dev/null +++ b/packages/entity-database-adapter-knex/src/internal/getKnexDataManager.ts @@ -0,0 +1,43 @@ +import { EntityDatabaseAdapter, EntityTableDataCoordinator } from '@expo/entity'; +import assert from 'assert'; + +import { BasePostgresEntityDatabaseAdapter } from '../BasePostgresEntityDatabaseAdapter'; +import { EntityKnexDataManager } from './EntityKnexDataManager'; +import { computeIfAbsentInWeakMap } from './weakMaps'; + +const knexDataManagerCache = new WeakMap< + EntityTableDataCoordinator, + EntityKnexDataManager +>(); + +export function getKnexDataManager< + TFields extends Record, + TIDField extends keyof TFields, +>( + tableDataCoordinator: EntityTableDataCoordinator, +): EntityKnexDataManager { + return computeIfAbsentInWeakMap( + knexDataManagerCache, + tableDataCoordinator, + (coordinator) => + new EntityKnexDataManager( + coordinator.entityConfiguration, + requireBasePostgresAdapter(coordinator.databaseAdapter), + coordinator.metricsAdapter, + coordinator.entityClassName, + ), + ); +} + +function requireBasePostgresAdapter< + TFields extends Record, + TIDField extends keyof TFields, +>( + databaseAdapter: EntityDatabaseAdapter, +): BasePostgresEntityDatabaseAdapter { + assert( + databaseAdapter instanceof BasePostgresEntityDatabaseAdapter, + `Cannot create KnexDataManager for EntityTableDataCoordinator with non-Postgres database adapter.`, + ); + return databaseAdapter; +} diff --git a/packages/entity-database-adapter-knex/src/internal/getKnexEntityLoaderFactory.ts b/packages/entity-database-adapter-knex/src/internal/getKnexEntityLoaderFactory.ts new file mode 100644 index 000000000..ba9dc6764 --- /dev/null +++ b/packages/entity-database-adapter-knex/src/internal/getKnexEntityLoaderFactory.ts @@ -0,0 +1,59 @@ +import { + EntityCompanion, + EntityPrivacyPolicy, + IEntityClass, + ReadonlyEntity, + ViewerContext, +} from '@expo/entity'; + +import { KnexEntityLoaderFactory } from '../KnexEntityLoaderFactory'; +import { getKnexDataManager } from './getKnexDataManager'; +import { computeIfAbsentInWeakMap } from './weakMaps'; + +const knexEntityLoaderFactoryCache = new WeakMap< + EntityCompanion, + KnexEntityLoaderFactory +>(); + +export function getKnexEntityLoaderFactory< + TFields extends Record, + TIDField extends keyof NonNullable>, + TViewerContext extends ViewerContext, + TEntity extends ReadonlyEntity, + TPrivacyPolicy extends EntityPrivacyPolicy< + TFields, + TIDField, + TViewerContext, + TEntity, + TSelectedFields + >, + TSelectedFields extends keyof TFields = keyof TFields, +>( + entityClass: IEntityClass< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + >, + viewerContext: TViewerContext, +): KnexEntityLoaderFactory< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields +> { + return computeIfAbsentInWeakMap( + knexEntityLoaderFactoryCache, + viewerContext.entityCompanionProvider.getCompanionForEntity(entityClass), + (companion) => + new KnexEntityLoaderFactory( + companion, + getKnexDataManager(companion.tableDataCoordinator), + companion.metricsAdapter, + ), + ); +} diff --git a/packages/entity-database-adapter-knex/src/internal/weakMaps.ts b/packages/entity-database-adapter-knex/src/internal/weakMaps.ts new file mode 100644 index 000000000..41855433e --- /dev/null +++ b/packages/entity-database-adapter-knex/src/internal/weakMaps.ts @@ -0,0 +1,19 @@ +/** + * If the specified key is not already associated with a value in this weak map, computes + * its value using the given mapping function and enters it into this map. + * + * @param map - map from which to get the key's value or compute and associate + * @param key - key for which to get the value or with which the computed value is to be associated + * @param mappingFunction - function to compute a value for key + */ +export const computeIfAbsentInWeakMap = ( + map: WeakMap, + key: K, + mappingFunction: (key: K) => V, +): V => { + if (!map.has(key)) { + const value = mappingFunction(key); + map.set(key, value); + } + return map.get(key)!; +}; diff --git a/packages/entity-database-adapter-knex/src/knexLoader.ts b/packages/entity-database-adapter-knex/src/knexLoader.ts new file mode 100644 index 000000000..0a30022fb --- /dev/null +++ b/packages/entity-database-adapter-knex/src/knexLoader.ts @@ -0,0 +1,108 @@ +import { + EntityPrivacyPolicy, + EntityQueryContext, + IEntityClass, + ReadonlyEntity, + ViewerContext, +} from '@expo/entity'; + +import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader'; +import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader'; +import { getKnexEntityLoaderFactory } from './internal/getKnexEntityLoaderFactory'; + +/** + * Vend knex loader for loading entities via non-data-loader methods in a given query context. + * @param entityClass - entity class to load + * @param viewerContext - viewer context of loading user + * @param queryContext - query context in which to perform the load + */ +export function knexLoader< + TMFields extends object, + TMIDField extends keyof NonNullable>, + TMViewerContext extends ViewerContext, + TMEntity extends ReadonlyEntity, + TMPrivacyPolicy extends EntityPrivacyPolicy< + TMFields, + TMIDField, + TMViewerContext, + TMEntity, + TMSelectedFields + >, + TMSelectedFields extends keyof TMFields = keyof TMFields, +>( + entityClass: IEntityClass< + TMFields, + TMIDField, + TMViewerContext, + TMEntity, + TMPrivacyPolicy, + TMSelectedFields + >, + viewerContext: TMViewerContext, + queryContext: EntityQueryContext = viewerContext + .getViewerScopedEntityCompanionForClass(entityClass) + .getQueryContextProvider() + .getQueryContext(), +): EnforcingKnexEntityLoader< + TMFields, + TMIDField, + TMViewerContext, + TMEntity, + TMPrivacyPolicy, + TMSelectedFields +> { + return getKnexEntityLoaderFactory(entityClass, viewerContext).forLoadEnforcing( + viewerContext, + queryContext, + { previousValue: null, cascadingDeleteCause: null }, + ); +} + +/** + * Vend knex loader for loading entities via non-data-loader methods in a given query context. + * Returns authorization results instead of throwing on authorization errors. + * @param entityClass - entity class to load + * @param viewerContext - viewer context of loading user + * @param queryContext - query context in which to perform the load + */ +export function knexLoaderWithAuthorizationResults< + TMFields extends object, + TMIDField extends keyof NonNullable>, + TMViewerContext extends ViewerContext, + TMEntity extends ReadonlyEntity, + TMPrivacyPolicy extends EntityPrivacyPolicy< + TMFields, + TMIDField, + TMViewerContext, + TMEntity, + TMSelectedFields + >, + TMSelectedFields extends keyof TMFields = keyof TMFields, +>( + entityClass: IEntityClass< + TMFields, + TMIDField, + TMViewerContext, + TMEntity, + TMPrivacyPolicy, + TMSelectedFields + >, + viewerContext: TMViewerContext, + queryContext: EntityQueryContext = viewerContext + .getViewerScopedEntityCompanionForClass(entityClass) + .getQueryContextProvider() + .getQueryContext(), +): AuthorizationResultBasedKnexEntityLoader< + TMFields, + TMIDField, + TMViewerContext, + TMEntity, + TMPrivacyPolicy, + TMSelectedFields +> { + return getKnexEntityLoaderFactory(entityClass, viewerContext).forLoad( + viewerContext, + queryContext, + { previousValue: null, cascadingDeleteCause: null }, + ); +} diff --git a/packages/entity-example/src/adapters/InMemoryDatabaseAdapter.ts b/packages/entity-example/src/adapters/InMemoryDatabaseAdapter.ts index 216fd3865..4cacddc32 100644 --- a/packages/entity-example/src/adapters/InMemoryDatabaseAdapter.ts +++ b/packages/entity-example/src/adapters/InMemoryDatabaseAdapter.ts @@ -10,14 +10,6 @@ import { v4 as uuidv4 } from 'uuid'; const dbObjects: Readonly<{ [key: string]: any }>[] = []; export class InMemoryDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { - getExtensionsKey(): string { - return 'InMemoryDatabaseAdapterProvider'; - } - - installExtensions(): void { - // No-op - } - getDatabaseAdapter, TIDField extends keyof TFields>( entityConfiguration: EntityConfiguration, ): EntityDatabaseAdapter { diff --git a/packages/entity-testing-utils/src/StubDatabaseAdapterProvider.ts b/packages/entity-testing-utils/src/StubDatabaseAdapterProvider.ts index 48c3d4834..15535a134 100644 --- a/packages/entity-testing-utils/src/StubDatabaseAdapterProvider.ts +++ b/packages/entity-testing-utils/src/StubDatabaseAdapterProvider.ts @@ -7,14 +7,6 @@ import { import { StubDatabaseAdapter } from './StubDatabaseAdapter'; export class StubDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { - getExtensionsKey(): string { - return 'StubDatabaseAdapterProvider'; - } - - installExtensions(): void { - // No-op - } - private readonly objectCollection = new Map(); getDatabaseAdapter, TIDField extends keyof TFields>( diff --git a/packages/entity/src/EntityCompanionProvider.ts b/packages/entity/src/EntityCompanionProvider.ts index 6334316ac..2935285a2 100644 --- a/packages/entity/src/EntityCompanionProvider.ts +++ b/packages/entity/src/EntityCompanionProvider.ts @@ -11,7 +11,6 @@ import { IEntityCacheAdapterProvider } from './IEntityCacheAdapterProvider'; import { IEntityDatabaseAdapterProvider } from './IEntityDatabaseAdapterProvider'; import { ReadonlyEntity } from './ReadonlyEntity'; import { ViewerContext } from './ViewerContext'; -import { ViewerScopedEntityCompanion } from './ViewerScopedEntityCompanion'; import { EntityTableDataCoordinator } from './internal/EntityTableDataCoordinator'; import { IEntityMetricsAdapter } from './metrics/IEntityMetricsAdapter'; import { computeIfAbsent } from './utils/collections/maps'; @@ -134,7 +133,6 @@ export class EntityCompanionProvider { new Map(); private readonly tableDataCoordinatorMap: Map> = new Map(); - private static readonly installedExtensions = new Set(); /** * Instantiate an Entity framework. @@ -160,26 +158,7 @@ export class EntityCompanionProvider { any, any > = {}, - ) { - // Install any extensions required by the database adapter flavors - for (const flavorDefinition of databaseAdapterFlavors.values()) { - if ( - !EntityCompanionProvider.installedExtensions.has( - flavorDefinition.adapterProvider.getExtensionsKey(), - ) - ) { - flavorDefinition.adapterProvider.installExtensions({ - EntityCompanionClass: EntityCompanion, - EntityTableDataCoordinatorClass: EntityTableDataCoordinator, - ViewerScopedEntityCompanionClass: ViewerScopedEntityCompanion, - ReadonlyEntityClass: ReadonlyEntity, - }); - EntityCompanionProvider.installedExtensions.add( - flavorDefinition.adapterProvider.getExtensionsKey(), - ); - } - } - } + ) {} /** * Get the entity companion for specified entity. If not already computed and cached, the entity diff --git a/packages/entity/src/IEntityDatabaseAdapterProvider.ts b/packages/entity/src/IEntityDatabaseAdapterProvider.ts index f010771fb..04c7e0aff 100644 --- a/packages/entity/src/IEntityDatabaseAdapterProvider.ts +++ b/packages/entity/src/IEntityDatabaseAdapterProvider.ts @@ -1,37 +1,13 @@ /* c8 ignore start - interface only */ -import { EntityCompanion } from './EntityCompanion'; import { EntityConfiguration } from './EntityConfiguration'; import { EntityDatabaseAdapter } from './EntityDatabaseAdapter'; -import { ReadonlyEntity } from './ReadonlyEntity'; -import { ViewerScopedEntityCompanion } from './ViewerScopedEntityCompanion'; -import { EntityTableDataCoordinator } from './internal/EntityTableDataCoordinator'; /** * A database adapter provider vends database adapters for a particular database adapter type. * Allows for passing global configuration to databse adapters, making testing easier. */ export interface IEntityDatabaseAdapterProvider { - /** - * A unique key for this type of adapter provider, used to avoid installing extensions multiple times. - */ - getExtensionsKey(): string; - - /** - * Install any necessary extensions to the Entity system. - */ - installExtensions({ - EntityCompanionClass, - EntityTableDataCoordinatorClass, - ViewerScopedEntityCompanionClass, - ReadonlyEntityClass, - }: { - EntityCompanionClass: typeof EntityCompanion; - EntityTableDataCoordinatorClass: typeof EntityTableDataCoordinator; - ViewerScopedEntityCompanionClass: typeof ViewerScopedEntityCompanion; - ReadonlyEntityClass: typeof ReadonlyEntity; - }): void; - /** * Vend a database adapter. */ diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index a210c5ddf..81c8bc176 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -370,12 +370,6 @@ const createEntityMutatorFactory = ( ), ); const customStubDatabaseAdapterProvider: IEntityDatabaseAdapterProvider = { - getExtensionsKey() { - return 'CustomStubDatabaseAdapterProvider'; - }, - - installExtensions: () => {}, - getDatabaseAdapter, TIDField extends keyof TFields>( _entityConfiguration: EntityConfiguration, ): EntityDatabaseAdapter { diff --git a/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts b/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts index 736ffe833..15c7a64e2 100644 --- a/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts +++ b/packages/entity/src/utils/__testfixtures__/StubDatabaseAdapterProvider.ts @@ -4,14 +4,6 @@ import { IEntityDatabaseAdapterProvider } from '../../IEntityDatabaseAdapterProv import { StubDatabaseAdapter } from '../__testfixtures__/StubDatabaseAdapter'; export class StubDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider { - getExtensionsKey(): string { - return 'StubDatabaseAdapterProvider'; - } - - installExtensions(): void { - // No-op - } - private readonly objectCollection = new Map(); getDatabaseAdapter, TIDField extends keyof TFields>( diff --git a/packages/entity/src/utils/collections/maps.ts b/packages/entity/src/utils/collections/maps.ts index 0ad3d3d8e..10f7dc6d2 100644 --- a/packages/entity/src/utils/collections/maps.ts +++ b/packages/entity/src/utils/collections/maps.ts @@ -1,8 +1,8 @@ import invariant from 'invariant'; /** - * If the specified key is not already associated with a value in this map, attempts to compute - * its value using the given mapping function and enters it into this map unless null. + * If the specified key is not already associated with a value in this map, computes + * its value using the given mapping function and enters it into this map. * * @param map - map from which to get the key's value or compute and associate * @param key - key for which to get the value or with which the computed value is to be associated