Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1618,8 +1618,6 @@ describe('postgres entity integration', () => {
);

// Create test data with names that sort in a specific order
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

const entities = [];
for (let i = 1; i <= 5; i++) {
Expand Down Expand Up @@ -1673,8 +1671,6 @@ describe('postgres entity integration', () => {
);

// Create entities with duplicate values to test stability
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

const entities = [];
for (let i = 1; i <= 6; i++) {
Expand Down Expand Up @@ -1777,9 +1773,6 @@ describe('postgres entity integration', () => {
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);

await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Create entities with different names
const names = ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'];
for (const name of names) {
Expand Down Expand Up @@ -1841,9 +1834,6 @@ describe('postgres entity integration', () => {
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);

await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Create exactly 6 entities
for (let i = 1; i <= 6; i++) {
await PostgresTestEntity.creator(vc).setField('name', `Entity${i}`).createAsync();
Expand Down Expand Up @@ -1879,8 +1869,6 @@ describe('postgres entity integration', () => {
const vc = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Create test data with specific order
await PostgresTestEntity.creator(vc).setField('name', 'Charlie').createAsync();
Expand Down Expand Up @@ -1910,8 +1898,6 @@ describe('postgres entity integration', () => {
const vc = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Enable pg_trgm extension for trigram similarity
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
Expand Down Expand Up @@ -2091,15 +2077,56 @@ describe('postgres entity integration', () => {
});
});

it('returns empty page when cursor entity no longer exists', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));

// Create test entities
const names = ['Alice', 'Bob', 'Charlie', 'David', 'Eve'];
for (const name of names) {
await PostgresTestEntity.creator(vc).setField('name', name).createAsync();
}

// Get first page and capture cursor pointing to a specific entity
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
first: 2,
pagination: {
strategy: PaginationStrategy.STANDARD,
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
},
});

expect(firstPage.edges).toHaveLength(2);
const cursorEntityNode = firstPage.edges[1]!.node; // 'Bob'
const cursor = firstPage.pageInfo.endCursor!;

// Delete the entity that the cursor refers to
await PostgresTestEntity.deleter(cursorEntityNode).deleteAsync();

// Paginate using the cursor of the now-deleted entity
const result = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
first: 10,
after: cursor,
pagination: {
strategy: PaginationStrategy.STANDARD,
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
},
});

expect(result.edges).toEqual([]);
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
});
});

describe(PaginationStrategy.ILIKE_SEARCH, () => {
it('supports search with ILIKE strategy', async () => {
const vc = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);

await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Create test data with searchable names
const names = [
'Alice Johnson',
Expand Down Expand Up @@ -2196,8 +2223,6 @@ describe('postgres entity integration', () => {
const vc = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Create test data
const names = ['Apple', 'Application', 'Apply', 'Banana', 'Cherry', 'Pineapple'];
Expand Down Expand Up @@ -2388,9 +2413,6 @@ describe('postgres entity integration', () => {
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);

await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Enable pg_trgm extension for trigram similarity
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');

Expand Down Expand Up @@ -2448,8 +2470,6 @@ describe('postgres entity integration', () => {
const vc = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Enable pg_trgm extension for trigram similarity
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
Expand Down Expand Up @@ -2552,8 +2572,6 @@ describe('postgres entity integration', () => {
const vc = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Enable pg_trgm extension for trigram similarity
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
Expand Down Expand Up @@ -2721,8 +2739,6 @@ describe('postgres entity integration', () => {
const vc = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await PostgresTestEntity.dropPostgresTableAsync(knexInstance);
await PostgresTestEntity.createOrTruncatePostgresTableAsync(knexInstance);

// Enable pg_trgm extension for trigram similarity
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ export class EntityKnexDataManager<
/**
* Load a page of objects using cursor-based pagination with unified pagination specification.
*
* @remarks
*
* This method implements cursor-based pagination using the seek method for efficient pagination even on large datasets
* given appropriate indexes. Cursors are opaque and encode the necessary information to fetch the next page based on the
* specified pagination strategy (standard, ILIKE search, or trigram search). For this implementation in particular,
* the cursor encodes the ID of the last entity in the page to ensure correct pagination for all strategies, even in cases
* where multiple rows have the same value for all fields other than the ID. If the entity referenced by a cursor has been
* deleted, the load will return an empty page with `hasNextPage: false`.
*
* @param queryContext - query context in which to perform the load
* @param args - pagination arguments including pagination and first/after or last/before
* @returns connection with edges containing field objects and page info
Expand Down Expand Up @@ -477,6 +486,8 @@ export class EntityKnexDataManager<
// We build a tuple comparison for fieldsToUseInPostgresTupleCursor fields of the
// entity identified by the external cursor to ensure correct pagination behavior
// even in cases where multiple rows have the same value all fields other than id.
// If the cursor entity has been deleted, the subquery returns no rows and the
// comparison evaluates to NULL, filtering out all results (empty page).
const operator = direction === PaginationDirection.FORWARD ? '>' : '<';

const idField = getDatabaseFieldForEntityField(
Expand Down Expand Up @@ -555,7 +566,9 @@ export class EntityKnexDataManager<
decodedExternalCursorEntityID: TFields[TIDField],
direction: PaginationDirection,
): SQLFragment {
// For TRIGRAM search, we compute the similarity values using a subquery, similar to normal cursor
// For TRIGRAM search, we compute the similarity values using a subquery, similar to normal cursor.
// If the cursor entity has been deleted, the subquery returns no rows and the
// comparison evaluates to NULL, filtering out all results (empty page).
const operator = direction === PaginationDirection.FORWARD ? '<' : '>';
const idField = getDatabaseFieldForEntityField(
this.entityConfiguration,
Expand Down