From 4b5261119192f4726e1d9c0285400c06264a10ad Mon Sep 17 00:00:00 2001 From: Will Schurman Date: Thu, 26 Feb 2026 11:49:18 -0800 Subject: [PATCH] feat: add method to get pagination cursor for single entity --- ...uthorizationResultBasedKnexEntityLoader.ts | 8 ++++ .../src/EnforcingKnexEntityLoader.ts | 12 +++++ .../PostgresEntityIntegration-test.ts | 46 +++++++++++++++++++ .../src/internal/EntityKnexDataManager.ts | 4 ++ 4 files changed, 70 insertions(+) diff --git a/packages/entity-database-adapter-knex/src/AuthorizationResultBasedKnexEntityLoader.ts b/packages/entity-database-adapter-knex/src/AuthorizationResultBasedKnexEntityLoader.ts index 9537fca37..01996fb74 100644 --- a/packages/entity-database-adapter-knex/src/AuthorizationResultBasedKnexEntityLoader.ts +++ b/packages/entity-database-adapter-knex/src/AuthorizationResultBasedKnexEntityLoader.ts @@ -477,6 +477,14 @@ export class AuthorizationResultBasedKnexEntityLoader< pageInfo, }; } + + /** + * Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name. + * @returns The pagination cursor for the given entity. + */ + getPaginationCursorForEntity(entity: TEntity): string { + return this.knexDataManager.getCursorForEntityID(entity.getID()); + } } /** diff --git a/packages/entity-database-adapter-knex/src/EnforcingKnexEntityLoader.ts b/packages/entity-database-adapter-knex/src/EnforcingKnexEntityLoader.ts index b52c4dab7..90c8a03e9 100644 --- a/packages/entity-database-adapter-knex/src/EnforcingKnexEntityLoader.ts +++ b/packages/entity-database-adapter-knex/src/EnforcingKnexEntityLoader.ts @@ -210,6 +210,18 @@ export class EnforcingKnexEntityLoader< pageInfo: pageResult.pageInfo, }; } + + /** + * Get cursor for a given entity that matches what loadPageAsync would produce. + * Useful for constructing pagination cursors for entities returned from other loader methods that can then be passed to loadPageAsync for pagination. + * Most commonly used for testing pagination behavior. + * + * @param entity - The entity to get the pagination cursor for. + * @returns The pagination cursor for the given entity. + */ + getPaginationCursorForEntity(entity: TEntity): string { + return this.knexEntityLoader.getPaginationCursorForEntity(entity); + } } /** 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 3a1e54915..92f95e4ab 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 @@ -1504,6 +1504,52 @@ describe('postgres entity integration', () => { expect(secondPage.pageInfo.hasPreviousPage).toBe(false); }); + it('getPaginationCursorForEntity produces cursor usable with loadPageAsync', async () => { + const vc = new ViewerContext( + createKnexIntegrationTestEntityCompanionProvider(knexInstance), + ); + + // Load first page to get the third entity (Charlie) + const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + first: 3, + pagination: { + strategy: PaginationStrategy.STANDARD, + orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }], + }, + }); + + const charlieEntity = firstPage.edges[2]!.node; + const cursorFromPage = firstPage.edges[2]!.cursor; + + // Get cursor using getPaginationCursorForEntity + const cursorFromMethod = + PostgresTestEntity.knexLoader(vc).getPaginationCursorForEntity(charlieEntity); + + // cursors should be equal for both loaders + expect(cursorFromMethod).toEqual( + PostgresTestEntity.knexLoaderWithAuthorizationResults(vc).getPaginationCursorForEntity( + charlieEntity, + ), + ); + + expect(cursorFromMethod).toBe(cursorFromPage); + + // Use the cursor from getPaginationCursorForEntity to paginate + const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({ + first: 3, + after: cursorFromMethod, + pagination: { + strategy: PaginationStrategy.STANDARD, + orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }], + }, + }); + + expect(nextPage.edges).toHaveLength(3); + expect(nextPage.edges[0]?.node.getField('name')).toBe('David'); + expect(nextPage.edges[1]?.node.getField('name')).toBe('Eve'); + expect(nextPage.edges[2]?.node.getField('name')).toBe('Frank'); + }); + it('performs backward pagination with last/before', async () => { const vc = new ViewerContext( createKnexIntegrationTestEntityCompanionProvider(knexInstance), diff --git a/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts b/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts index 68f00f4c7..9268c77aa 100644 --- a/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts +++ b/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts @@ -361,6 +361,10 @@ export class EntityKnexDataManager< } } + getCursorForEntityID(entityID: TFields[TIDField]): string { + return this.encodeOpaqueCursor(entityID); + } + /** * Internal method for loading a page with cursor-based pagination. * Shared logic for both regular and search pagination.