From 320786ccc99f90e6f679b93270fbac06ff6f9a89 Mon Sep 17 00:00:00 2001 From: Will Schurman Date: Fri, 13 Feb 2026 14:40:15 -0700 Subject: [PATCH 1/3] fix: refactor install method to instead be free functions --- .../v0.55.0-v0.56.0/test1.output.ts | 6 +- .../v0.55.0-v0.56.0/test2.output.ts | 7 +- .../src/transforms/v0.55.0-v0.56.0.ts | 100 +++++++++++++++--- 3 files changed, 96 insertions(+), 17 deletions(-) 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(); } From 6e85069c579c9ca3292a5c292adc318699d7fbe0 Mon Sep 17 00:00:00 2001 From: Will Schurman Date: Fri, 13 Feb 2026 16:04:01 -0700 Subject: [PATCH 2/3] fix: revert change to codemod now that Entity.knexLoader is supported again via subclass --- .../v0.55.0-v0.56.0/test1.output.ts | 6 +- .../v0.55.0-v0.56.0/test2.output.ts | 7 +- .../src/transforms/v0.55.0-v0.56.0.ts | 100 +++--------------- 3 files changed, 17 insertions(+), 96 deletions(-) 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 e88c54a50..c16f3d612 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,12 +2,10 @@ 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 = knexLoader(PostEntity, viewerContext); + const postLoader = PostEntity.knexLoader(viewerContext); // These use knex-specific methods, so they should be transformed const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([ @@ -18,7 +16,7 @@ async function loadUser(viewerContext: ViewerContext) { ]); // Loader with authorization results - only transformed when using knex methods - const userLoaderWithAuth = knexLoaderWithAuthorizationResults(UserEntity, viewerContext); + const userLoaderWithAuth = UserEntity.knexLoaderWithAuthorizationResults(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 31dbd5523..4c61115d1 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,12 +1,10 @@ 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 knexLoader(CommentEntity, viewerContext) + const comments = await CommentEntity.knexLoader(viewerContext) .loadManyByFieldEqualityConjunctionAsync([ { fieldName: 'postId', fieldValue: '123' } ]); @@ -17,7 +15,8 @@ const loadComments = async (viewerContext: ViewerContext) => { .loadByIDAsync('456'); // With authorization results and knex method - const commentsWithAuth = await knexLoaderWithAuthorizationResults(CommentEntity, viewerContext) + const commentsWithAuth = await CommentEntity + .knexLoaderWithAuthorizationResults(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 3e65e6480..92bf3f8cf 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,9 +79,7 @@ function isKnexSpecificMethodUsed(j: API['jscodeshift'], node: any): boolean { return false; } -function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection): boolean { - let transformed = false; - +function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection): void { // Find all entity expressions of the form `Entity.loader(viewerContext)` root .find(j.CallExpression, { @@ -107,28 +105,20 @@ function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection, -): boolean { - let transformed = false; - +): void { // Find all entity expressions of the form `Entity.loaderWithAuthorizationResults(viewerContext)` root .find(j.CallExpression, { @@ -154,88 +144,22 @@ function transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationRes if (firstChar === firstChar?.toUpperCase()) { // Check if this loader uses knex-specific methods if (isKnexSpecificMethodUsed(j, path)) { - // 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; + // Rename loaderWithAuthorizationResults to knexLoaderWithAuthorizationResults + if (loaderCallee.property.type === 'Identifier') { + loaderCallee.property.name = 'knexLoaderWithAuthorizationResults'; + } } } } }); - - 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); - const needsKnexLoader = transformLoaderToKnexLoader(j, root); - const needsKnexLoaderWithAuthorizationResults = - transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(j, root); - - addKnexImportIfNeeded(j, root, needsKnexLoader, needsKnexLoaderWithAuthorizationResults); + transformLoaderToKnexLoader(j, root); + transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(j, root); return root.toSource(); } From 762ccb8e9addeff83c78ed95372081794e5ca6f4 Mon Sep 17 00:00:00 2001 From: Will Schurman Date: Sat, 14 Feb 2026 11:21:34 -0700 Subject: [PATCH 3/3] fix: update integration test to use BlahEntity.knexLoader syntax --- .../PostgresEntityIntegration-test.ts | 201 ++++++++---------- .../__testfixtures__/PostgresTestEntity.ts | 8 +- .../src/__tests__/ReadonlyEntity-test.ts | 4 + .../src/__tests__/fixtures/TestEntity.ts | 5 +- .../fixtures/TestPaginationEntity.ts | 9 +- 5 files changed, 114 insertions(+), 113 deletions(-) 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 f6a576601..62ff0487e 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,7 +17,6 @@ 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; @@ -430,7 +429,7 @@ describe('postgres entity integration', () => { ); // Test basic SQL query with parameters - const catOwners = await knexLoader(PostgresTestEntity, vc1) + const catOwners = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`has_a_cat = ${true}`) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -440,7 +439,7 @@ describe('postgres entity integration', () => { expect(catOwners[1]!.getField('name')).toBe('Charlie'); // Test with limit and offset - const limitedResults = await knexLoader(PostgresTestEntity, vc1) + const limitedResults = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`has_a_cat = ${true}`) .orderBy('name', OrderByOrdering.ASCENDING) .limit(1) @@ -480,7 +479,7 @@ describe('postgres entity integration', () => { ); // Test AND condition - const bothPets = await knexLoader(PostgresTestEntity, vc1) + const bothPets = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(and(eq('has_a_cat', true), eq('has_a_dog', true))) .executeAsync(); @@ -488,7 +487,7 @@ describe('postgres entity integration', () => { expect(bothPets[0]!.getField('name')).toBe('User3'); // Test OR condition - const eitherPet = await knexLoader(PostgresTestEntity, vc1) + const eitherPet = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(or(eq('has_a_cat', false), eq('has_a_dog', false))) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -498,7 +497,7 @@ describe('postgres entity integration', () => { expect(eitherPet[1]!.getField('name')).toBe('User2'); // Test IN array - const specificUsers = await knexLoader(PostgresTestEntity, vc1) + const specificUsers = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(inArray('name', ['User1', 'User3'])) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -508,7 +507,7 @@ describe('postgres entity integration', () => { expect(specificUsers[1]!.getField('name')).toBe('User3'); // Test complex condition - const complexQuery = await knexLoader(PostgresTestEntity, vc1) + const complexQuery = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(and(or(eq('has_a_cat', true), eq('has_a_dog', true)), neq('name', 'User2'))) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -535,7 +534,7 @@ describe('postgres entity integration', () => { .createAsync(), ); - const firstCatOwnerLimit1 = await knexLoader(PostgresTestEntity, vc1) + const firstCatOwnerLimit1 = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`has_a_cat = ${true}`) .orderBy('name', OrderByOrdering.ASCENDING) .limit(1) @@ -545,7 +544,7 @@ describe('postgres entity integration', () => { expect(firstCatOwnerLimit1[0]?.getField('name')).toBe('First'); // Test executeFirstAsync with no results - const noDogOwnerLimit1 = await knexLoader(PostgresTestEntity, vc1) + const noDogOwnerLimit1 = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`has_a_dog = ${true}`) .limit(1) .executeAsync(); @@ -571,7 +570,7 @@ describe('postgres entity integration', () => { ); // Test with authorization results - const results = await knexLoaderWithAuthorizationResults(PostgresTestEntity, vc1) + const results = await PostgresTestEntity.knexLoaderWithAuthorizationResults(vc1) .loadManyBySQL(sql`name LIKE ${'AuthTest%'}`) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -587,7 +586,7 @@ describe('postgres entity integration', () => { expect(results[1]!.value.getField('name')).toBe('AuthTest2'); } - const firstResultLimit1 = await knexLoaderWithAuthorizationResults(PostgresTestEntity, vc1) + const firstResultLimit1 = await PostgresTestEntity.knexLoaderWithAuthorizationResults(vc1) .loadManyBySQL(sql`has_a_cat = ${false}`) .limit(1) .executeAsync(); @@ -627,7 +626,7 @@ describe('postgres entity integration', () => { // Test raw SQL for dynamic column names with orderBySQL const sortColumn = 'name'; - const rawResults = await knexLoader(PostgresTestEntity, vc1) + const rawResults = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`${raw('name')} LIKE ${'RawTest%'}`) .orderBySQL(sql`${raw(sortColumn)} DESC`) .executeAsync(); @@ -638,7 +637,7 @@ describe('postgres entity integration', () => { expect(rawResults[2]!.getField('name')).toBe('RawTest1'); // Test complex ORDER BY with CASE statement - const priorityResults = await knexLoader(PostgresTestEntity, vc1) + const priorityResults = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`name LIKE ${'RawTest%'}`) .orderBySQL( sql`CASE @@ -655,7 +654,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 knexLoader(PostgresTestEntity, vc1) + const complexExpression = await PostgresTestEntity.knexLoader(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', @@ -689,7 +688,7 @@ describe('postgres entity integration', () => { sql`name = ${'JoinTest1'}`, sql`(has_a_cat = ${true} AND has_a_dog = ${true})`, ]; - const joinedResults = await knexLoader(PostgresTestEntity, vc1) + const joinedResults = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(SQLFragment.join(conditions, ' OR ')) .orderBy('name', OrderByOrdering.ASCENDING) .executeAsync(); @@ -759,7 +758,7 @@ describe('postgres entity integration', () => { ); // Test 1: Simple orderBySQL with raw column - const simpleOrder = await knexLoader(PostgresTestEntity, vc1) + const simpleOrder = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`${raw('name')} DESC`) .executeAsync(); @@ -775,7 +774,7 @@ describe('postgres entity integration', () => { const priority2 = 2; const priority3 = 3; const priority4 = 4; - const caseOrder = await knexLoader(PostgresTestEntity, vc1) + const caseOrder = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL( sql`CASE @@ -794,7 +793,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 knexLoader(PostgresTestEntity, vc1) + const arrayLengthOrder = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`COALESCE(array_length(string_array, 1), 0) DESC, ${raw('name')} ASC`) .executeAsync(); @@ -806,7 +805,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 knexLoader(PostgresTestEntity, vc1) + const multipleOrderBy = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`${raw('name')} DESC`) // This will be overridden .orderBySQL(sql`${raw('name')} ASC`) // This one wins @@ -817,7 +816,7 @@ describe('postgres entity integration', () => { expect(multipleOrderBy[3]!.getField('name')).toBe('OrderTest4'); // Test 5: Combining orderBySQL with limit and offset - const limitedOrder = await knexLoader(PostgresTestEntity, vc1) + const limitedOrder = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`${raw('name')} ASC`) .limit(2) @@ -829,7 +828,7 @@ describe('postgres entity integration', () => { expect(limitedOrder[1]!.getField('name')).toBe('OrderTest3'); // Test 6: orderBySQL with NULLS FIRST/LAST - const nullsOrder = await knexLoader(PostgresTestEntity, vc1) + const nullsOrder = await PostgresTestEntity.knexLoader(vc1) .loadManyBySQL(sql`name LIKE ${'OrderTest%'}`) .orderBySQL(sql`string_array IS NULL, ${raw('name')} ASC`) .executeAsync(); @@ -849,7 +848,7 @@ describe('postgres entity integration', () => { .createAsync(); // Create a query builder - const queryBuilder = knexLoader(PostgresTestEntity, vc1).loadManyBySQL( + const queryBuilder = PostgresTestEntity.knexLoader(vc1).loadManyBySQL( sql`name = ${'MultiExecTest'}`, ); @@ -869,7 +868,7 @@ describe('postgres entity integration', () => { ); // A new query builder should work fine - const newQueryBuilder = knexLoader(PostgresTestEntity, vc1).loadManyBySQL( + const newQueryBuilder = PostgresTestEntity.knexLoader(vc1).loadManyBySQL( sql`name = ${'MultiExecTest'}`, ); @@ -907,8 +906,7 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await knexLoader( - PostgresTestEntity, + const results = await PostgresTestEntity.knexLoader( vc1, ).loadManyByFieldEqualityConjunctionAsync([ { @@ -923,8 +921,7 @@ describe('postgres entity integration', () => { expect(results).toHaveLength(2); - const results2 = await knexLoader( - PostgresTestEntity, + const results2 = await PostgresTestEntity.knexLoader( vc1, ).loadManyByFieldEqualityConjunctionAsync([ { fieldName: 'hasADog', fieldValues: [true, false] }, @@ -947,8 +944,7 @@ describe('postgres entity integration', () => { PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'c').createAsync(), ); - const results = await knexLoader( - PostgresTestEntity, + const results = await PostgresTestEntity.knexLoader( vc1, ).loadManyByFieldEqualityConjunctionAsync([], { limit: 2, @@ -991,15 +987,13 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await knexLoader( - PostgresTestEntity, + const results = await PostgresTestEntity.knexLoader( vc1, ).loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: null }]); expect(results).toHaveLength(2); expect(results[0]!.getField('name')).toBeNull(); - const results2 = await knexLoader( - PostgresTestEntity, + const results2 = await PostgresTestEntity.knexLoader( vc1, ).loadManyByFieldEqualityConjunctionAsync( [ @@ -1031,7 +1025,7 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await knexLoader(PostgresTestEntity, vc1).loadManyByRawWhereClauseAsync( + const results = await PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync( 'name = ?', ['hello'], ); @@ -1050,7 +1044,7 @@ describe('postgres entity integration', () => { ); await expect( - knexLoader(PostgresTestEntity, vc1).loadManyByRawWhereClauseAsync('invalid_column = ?', [ + PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync('invalid_column = ?', [ 'hello', ]), ).rejects.toThrow(); @@ -1080,7 +1074,7 @@ describe('postgres entity integration', () => { .createAsync(), ); - const results = await knexLoader(PostgresTestEntity, vc1).loadManyByRawWhereClauseAsync( + const results = await PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync( 'has_a_dog = ?', [true], { @@ -1098,8 +1092,7 @@ describe('postgres entity integration', () => { expect(results).toHaveLength(2); expect(results.map((e) => e.getField('name'))).toEqual(['b', 'c']); - const resultsMultipleOrderBy = await knexLoader( - PostgresTestEntity, + const resultsMultipleOrderBy = await PostgresTestEntity.knexLoader( vc1, ).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], { orderBy: [ @@ -1117,8 +1110,7 @@ describe('postgres entity integration', () => { expect(resultsMultipleOrderBy).toHaveLength(3); expect(resultsMultipleOrderBy.map((e) => e.getField('name'))).toEqual(['c', 'b', 'a']); - const resultsOrderByRaw = await knexLoader( - PostgresTestEntity, + const resultsOrderByRaw = await PostgresTestEntity.knexLoader( vc1, ).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], { orderByRaw: 'has_a_dog ASC, name DESC', @@ -1394,7 +1386,7 @@ describe('postgres entity integration', () => { ); // Get first page - const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1410,7 +1402,7 @@ describe('postgres entity integration', () => { expect(firstPage.pageInfo.hasPreviousPage).toBe(false); // Get second page using cursor - const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, after: firstPage.pageInfo.endCursor!, pagination: { @@ -1433,7 +1425,7 @@ describe('postgres entity integration', () => { ); // Get last page - const lastPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1449,7 +1441,7 @@ describe('postgres entity integration', () => { expect(lastPage.pageInfo.hasPreviousPage).toBe(true); // Get previous page using cursor - const previousPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const previousPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 3, before: lastPage.pageInfo.startCursor!, pagination: { @@ -1472,7 +1464,7 @@ describe('postgres entity integration', () => { ); // Query only entities with cats - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 2, where: sql`has_a_cat = ${true}`, pagination: { @@ -1489,7 +1481,7 @@ describe('postgres entity integration', () => { expect(page.pageInfo.hasNextPage).toBe(true); // Get next page with same where condition - const nextPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 2, after: page.pageInfo.endCursor!, where: sql`has_a_cat = ${true}`, @@ -1512,7 +1504,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 4, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1541,7 +1533,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, where: sql`name = ${'NonexistentName'}`, pagination: { @@ -1562,7 +1554,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1576,7 +1568,7 @@ describe('postgres entity integration', () => { expect(page.edges[2]?.cursor).toBeTruthy(); // Start from middle item - const nextPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 2, after: page.edges[1]!.cursor, pagination: { @@ -1595,7 +1587,7 @@ describe('postgres entity integration', () => { createKnexIntegrationTestEntityCompanionProvider(knexInstance), ); - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1606,7 +1598,7 @@ describe('postgres entity integration', () => { expect(page.edges).toHaveLength(3); // Navigate using cursor - const nextPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, after: page.pageInfo.endCursor!, pagination: { @@ -1639,7 +1631,7 @@ describe('postgres entity integration', () => { // Test backward pagination with DESCENDING order // This internally flips DESCENDING to ASCENDING for the query - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1658,7 +1650,7 @@ describe('postgres entity integration', () => { expect(page.pageInfo.hasNextPage).toBe(false); // Verify the order is maintained correctly with forward pagination too - const forwardPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const forwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1694,7 +1686,7 @@ describe('postgres entity integration', () => { } // Pagination with only name in orderBy - ID should be added automatically for stability - const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1705,7 +1697,7 @@ describe('postgres entity integration', () => { expect(firstPage.edges).toHaveLength(3); // Get second page - const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, after: firstPage.pageInfo.endCursor!, pagination: { @@ -1723,7 +1715,7 @@ describe('postgres entity integration', () => { expect(intersection).toHaveLength(0); // Test with explicit ID in orderBy (shouldn't duplicate) - const pageWithExplicitId = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const pageWithExplicitId = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1741,7 +1733,7 @@ describe('postgres entity integration', () => { // Try with completely invalid cursor await expect( - knexLoader(PostgresTestEntity, vc).loadPageAsync({ + PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, after: 'not-a-valid-cursor', pagination: { @@ -1754,7 +1746,7 @@ describe('postgres entity integration', () => { // Try with valid base64 but invalid JSON const invalidJsonCursor = Buffer.from('not json').toString('base64url'); await expect( - knexLoader(PostgresTestEntity, vc).loadPageAsync({ + PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, after: invalidJsonCursor, pagination: { @@ -1769,7 +1761,7 @@ describe('postgres entity integration', () => { 'base64url', ); await expect( - knexLoader(PostgresTestEntity, vc).loadPageAsync({ + PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, after: missingFieldsCursor, pagination: { @@ -1795,7 +1787,7 @@ describe('postgres entity integration', () => { } // Test with enforcing loader (standard pagination) - const pageEnforced = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const pageEnforced = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 4, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1811,7 +1803,7 @@ describe('postgres entity integration', () => { expect(pageEnforced.edges[3]?.node.getField('name')).toBe('David'); // Test pagination continues correctly - const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 4, after: pageEnforced.pageInfo.endCursor!, pagination: { @@ -1827,8 +1819,7 @@ 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 knexLoaderWithAuthorizationResults( - PostgresTestEntity, + const pageWithAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( vc, ).loadPageAsync({ first: 3, @@ -1859,7 +1850,7 @@ describe('postgres entity integration', () => { } // Load with limit 5 - should have hasNextPage=true - const page1 = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page1 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 5, pagination: { strategy: PaginationStrategy.STANDARD, @@ -1871,7 +1862,7 @@ describe('postgres entity integration', () => { expect(page1.pageInfo.hasNextPage).toBe(true); // Load the last entity - const page2 = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page2 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 5, after: page1.pageInfo.endCursor!, pagination: { @@ -1900,7 +1891,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 knexLoader(PostgresTestEntity, vc) + const results = await PostgresTestEntity.knexLoader(vc) .loadManyBySQL(sql`1 = 1`, { orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }], orderByFragment: sql`name DESC`, @@ -1944,7 +1935,7 @@ describe('postgres entity integration', () => { } // Test 1: Regular loader with ILIKE search - const iLikeSearchRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const iLikeSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 2, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -1959,8 +1950,7 @@ describe('postgres entity integration', () => { expect(iLikeSearchRegular.pageInfo.hasNextPage).toBe(true); // Test 2: Authorization result loader with same ILIKE search - const iLikeSearchAuth = await knexLoaderWithAuthorizationResults( - PostgresTestEntity, + const iLikeSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( vc, ).loadPageAsync({ first: 2, @@ -1978,7 +1968,7 @@ describe('postgres entity integration', () => { expect(iLikeSearchAuth.pageInfo.hasNextPage).toBe(true); // Test 3: Regular loader with TRIGRAM search - const trigramSearchRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const trigramSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -1996,8 +1986,7 @@ describe('postgres entity integration', () => { expect(foundNames).toContain('Frank Johnson'); // Test 4: Authorization result loader with TRIGRAM search - const trigramSearchAuth = await knexLoaderWithAuthorizationResults( - PostgresTestEntity, + const trigramSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( vc, ).loadPageAsync({ first: 3, @@ -2016,7 +2005,7 @@ describe('postgres entity integration', () => { expect(foundNamesAuth).toContain('Frank Johnson'); // Test 5: Test pagination with cursor for both loader types - const firstPageRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPageRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 1, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2028,7 +2017,7 @@ describe('postgres entity integration', () => { expect(firstPageRegular.edges).toHaveLength(1); expect(firstPageRegular.edges[0]?.node.getField('name')).toBe('Bob Smith'); - const secondPageRegular = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPageRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 1, after: firstPageRegular.pageInfo.endCursor!, pagination: { @@ -2042,7 +2031,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 knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const filteredSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, where: sql`has_a_cat = ${true}`, pagination: { @@ -2059,8 +2048,7 @@ 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 knexLoaderWithAuthorizationResults( - PostgresTestEntity, + const filteredSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( vc, ).loadPageAsync({ first: 10, @@ -2077,7 +2065,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 knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const withRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 1, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2088,8 +2076,7 @@ describe('postgres entity integration', () => { expect(withRegular.edges).toHaveLength(1); - const withAuth = await knexLoaderWithAuthorizationResults( - PostgresTestEntity, + const withAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults( vc, ).loadPageAsync({ first: 1, @@ -2130,7 +2117,7 @@ describe('postgres entity integration', () => { } // Search for names containing "Johnson" - const searchResults = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const searchResults = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2144,7 +2131,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 knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const smithPage1 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 1, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2158,7 +2145,7 @@ describe('postgres entity integration', () => { expect(smithPage1.pageInfo.hasNextPage).toBe(true); // Get next page - const smithPage2 = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const smithPage2 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 1, after: smithPage1.pageInfo.endCursor!, pagination: { @@ -2173,7 +2160,7 @@ describe('postgres entity integration', () => { expect(smithPage2.pageInfo.hasNextPage).toBe(false); // Test partial match (case insensitive) - const partialMatch = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const partialMatch = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2187,7 +2174,7 @@ describe('postgres entity integration', () => { expect(partialMatch.edges[1]?.node.getField('name')).toBe('Eve Johnson'); // Test search with WHERE clause - const combinedFilter = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const combinedFilter = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, where: sql`has_a_cat = ${true}`, pagination: { @@ -2219,7 +2206,7 @@ describe('postgres entity integration', () => { } // Forward pagination with ILIKE search - const forwardPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const forwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 2, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2236,7 +2223,7 @@ describe('postgres entity integration', () => { }); // Backward pagination with ILIKE search - const backwardPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const backwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 2, pagination: { strategy: PaginationStrategy.ILIKE_SEARCH, @@ -2257,7 +2244,7 @@ describe('postgres entity integration', () => { let hasNext = true; while (hasNext) { - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, ...(cursor && { after: cursor }), pagination: { @@ -2316,7 +2303,7 @@ describe('postgres entity integration', () => { const pageSize = 3; while (true) { - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: pageSize, ...(cursor && { after: cursor }), pagination: { @@ -2357,7 +2344,7 @@ describe('postgres entity integration', () => { pageCount = 0; while (true) { - const page = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: pageSize, ...(backCursor && { before: backCursor }), pagination: { @@ -2417,7 +2404,7 @@ describe('postgres entity integration', () => { } // Search for similar names to "Johnson" using trigram - const trigramSearch = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const trigramSearch = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2438,7 +2425,7 @@ describe('postgres entity integration', () => { expect(foundNames).toContain('Johnsen'); // Test combining with WHERE clause - const filteredTrigram = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const filteredTrigram = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, where: sql`has_a_cat = ${true}`, pagination: { @@ -2488,7 +2475,7 @@ describe('postgres entity integration', () => { } // First page with trigram search (no cursor) - const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2507,7 +2494,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 knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, after: firstPageCursor!, pagination: { @@ -2525,7 +2512,7 @@ describe('postgres entity integration', () => { // being passed through the parallel query path // Test backward pagination with cursor - const lastPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 2, before: firstPageCursor!, pagination: { @@ -2542,7 +2529,7 @@ describe('postgres entity integration', () => { // Test with WHERE clause, cursor, and search const firstEdgeCursor = firstPage.edges[0]?.cursor; expect(firstEdgeCursor).toBeDefined(); - const filteredWithCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const filteredWithCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 2, after: firstEdgeCursor!, where: sql`has_a_cat = ${true}`, @@ -2593,7 +2580,7 @@ describe('postgres entity integration', () => { } // Test 1: Forward pagination (first) - const firstPageForward = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPageForward = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 4, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2614,7 +2601,7 @@ describe('postgres entity integration', () => { expect(forwardNames).not.toContain('Williams'); // Test 2: Backward pagination (last) - const lastPageBackward = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const lastPageBackward = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 4, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2636,7 +2623,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 knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPageForwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2655,7 +2642,7 @@ describe('postgres entity integration', () => { })); const firstPageForwardCursorIDs = firstPageForwardCursorData.map((d) => d.id); - const secondPageForwardCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPageForwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, after: firstPageForwardCursor.pageInfo.endCursor!, pagination: { @@ -2683,7 +2670,7 @@ describe('postgres entity integration', () => { expect(overlapForwardCursor).toHaveLength(0); // Test 4: test backward cursor pagination with trigram search - const firstPageBackwardCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPageBackwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2702,7 +2689,7 @@ describe('postgres entity integration', () => { const firstPageBackwardIDs = firstPageBackwardCursorData.map((d) => d.id); expect(firstPageBackwardIDs.length).toBeGreaterThan(0); - const secondPageBackwardCursor = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPageBackwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 3, before: firstPageBackwardCursor.pageInfo.startCursor!, pagination: { @@ -2760,7 +2747,7 @@ describe('postgres entity integration', () => { } // Test TRIGRAM search with extraOrderByFields for stable pagination - const firstPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2779,7 +2766,7 @@ describe('postgres entity integration', () => { // Get second page using cursor // With extraOrderByFields, cursor includes hasACat field which provides more stable pagination - const secondPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 3, after: firstPageCursor!, pagination: { @@ -2800,7 +2787,7 @@ describe('postgres entity integration', () => { expect(overlap).toHaveLength(0); // Test backward pagination with extraOrderByFields - const lastPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ last: 2, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, @@ -2815,7 +2802,7 @@ describe('postgres entity integration', () => { // Test that extraOrderByFields provides consistent ordering // Get all results in one go for comparison - const allResultsPage = await knexLoader(PostgresTestEntity, vc).loadPageAsync({ + const allResultsPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ first: 10, pagination: { strategy: PaginationStrategy.TRIGRAM_SEARCH, diff --git a/packages/entity-database-adapter-knex/src/__testfixtures__/PostgresTestEntity.ts b/packages/entity-database-adapter-knex/src/__testfixtures__/PostgresTestEntity.ts index ca7aaad81..333f1635f 100644 --- a/packages/entity-database-adapter-knex/src/__testfixtures__/PostgresTestEntity.ts +++ b/packages/entity-database-adapter-knex/src/__testfixtures__/PostgresTestEntity.ts @@ -2,7 +2,6 @@ import { AlwaysAllowPrivacyPolicyRule, BooleanField, DateField, - Entity, EntityCompanionDefinition, EntityConfiguration, EntityPrivacyPolicy, @@ -16,6 +15,7 @@ import { import { Knex } from 'knex'; import { BigIntField, JSONArrayField, MaybeJSONArrayField } from '../EntityFields'; +import { PostgresEntity } from '../PostgresEntity'; type PostgresTestEntityFields = { id: string; @@ -34,7 +34,11 @@ type PostgresTestEntityFields = { createdAt: Date; }; -export class PostgresTestEntity extends Entity { +export class PostgresTestEntity extends PostgresEntity< + PostgresTestEntityFields, + 'id', + ViewerContext +> { static defineCompanionDefinition(): EntityCompanionDefinition< PostgresTestEntityFields, 'id', 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 6d6e1e5ac..2facab4a4 100644 --- a/packages/entity-database-adapter-knex/src/__tests__/ReadonlyEntity-test.ts +++ b/packages/entity-database-adapter-knex/src/__tests__/ReadonlyEntity-test.ts @@ -13,6 +13,7 @@ describe('knexLoader', () => { const companionProvider = createUnitTestPostgresEntityCompanionProvider(); const viewerContext = new ViewerContext(companionProvider); expect(knexLoader(TestEntity, viewerContext)).toBeInstanceOf(EnforcingKnexEntityLoader); + expect(TestEntity.knexLoader(viewerContext)).toBeInstanceOf(EnforcingKnexEntityLoader); }); }); @@ -23,6 +24,9 @@ describe('knexLoader', () => { expect(knexLoaderWithAuthorizationResults(TestEntity, viewerContext)).toBeInstanceOf( AuthorizationResultBasedKnexEntityLoader, ); + expect(TestEntity.knexLoaderWithAuthorizationResults(viewerContext)).toBeInstanceOf( + AuthorizationResultBasedKnexEntityLoader, + ); }); }); }); diff --git a/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestEntity.ts b/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestEntity.ts index dc6aecd25..23662a933 100644 --- a/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestEntity.ts +++ b/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestEntity.ts @@ -1,5 +1,4 @@ import { - Entity, EntityCompanionDefinition, EntityConfiguration, DateField, @@ -12,6 +11,8 @@ import { } from '@expo/entity'; import { result, Result } from '@expo/results'; +import { PostgresEntity } from '../../PostgresEntity'; + export type TestFields = { customIdField: string; testIndexedField: string; @@ -75,7 +76,7 @@ export class TestEntityPrivacyPolicy extends EntityPrivacyPolicy< ]; } -export class TestEntity extends Entity { +export class TestEntity extends PostgresEntity { static defineCompanionDefinition(): EntityCompanionDefinition< TestFields, 'customIdField', diff --git a/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestPaginationEntity.ts b/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestPaginationEntity.ts index 213c54421..66c329ec7 100644 --- a/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestPaginationEntity.ts +++ b/packages/entity-database-adapter-knex/src/__tests__/fixtures/TestPaginationEntity.ts @@ -1,5 +1,4 @@ import { - Entity, EntityCompanionDefinition, EntityConfiguration, EntityPrivacyPolicy, @@ -13,6 +12,8 @@ import { RuleEvaluationResult, } from '@expo/entity'; +import { PostgresEntity } from '../../PostgresEntity'; + export interface TestPaginationFields { id: string; name: string; @@ -85,7 +86,11 @@ export class TestPaginationPrivacyPolicy extends EntityPrivacyPolicy< protected override readonly deleteRules = []; } -export class TestPaginationEntity extends Entity { +export class TestPaginationEntity extends PostgresEntity< + TestPaginationFields, + 'id', + ViewerContext +> { static defineCompanionDefinition(): EntityCompanionDefinition< TestPaginationFields, 'id',