diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index a02c74c0c1..b3e52fb480 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -203,6 +203,7 @@ export interface QueryEntitiesInitialRequest { term: string; fields?: string[]; }; + skipTotalItems?: boolean; } /** diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 418f503739..bb2ba3d587 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -1478,6 +1478,52 @@ describe('DefaultEntitiesCatalog', () => { }, ); + it.each(databases.eachSupportedId())( + 'can skip totalItems, %p', + async databaseId => { + await createDatabase(databaseId); + + await Promise.all( + Array(15) + .fill(0) + .map(() => + addEntityToSearch({ + apiVersion: 'a', + kind: 'k', + metadata: { name: v4() }, + }), + ), + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: mockServices.logger.mock(), + stitcher, + }); + + const request: QueryEntitiesInitialRequest = { + limit: 10, + credentials: mockCredentials.none(), + skipTotalItems: true, + }; + let response = await catalog.queryEntities(request); + expect(response).toEqual({ + totalItems: 0, + items: expect.objectContaining({ length: 10 }), + pageInfo: { nextCursor: expect.anything() }, + }); + response = await catalog.queryEntities({ + ...request, + cursor: response.pageInfo.nextCursor!, + }); + expect(response).toEqual({ + totalItems: 0, + items: expect.objectContaining({ length: 5 }), + pageInfo: { prevCursor: expect.anything() }, + }); + }, + ); + it.each(databases.eachSupportedId())( 'should paginate results accordingly in case of clashing items, %p', async databaseId => { diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index 2540fc1725..fa43eb79d2 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -331,6 +331,7 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { const cursor: Omit & { orderFieldValues?: (string | null)[]; + skipTotalItems: boolean; } = { orderFields: [], isPrevious: false, @@ -341,7 +342,8 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { // request. The result is then embedded into the cursor for subsequent // requests. Threfore this can be undefined here, but will then get // populated further down. - const shouldComputeTotalItems = cursor.totalItems === undefined; + const shouldComputeTotalItems = + cursor.totalItems === undefined && !cursor.skipTotalItems; const isFetchingBackwards = cursor.isPrevious; if (cursor.orderFields.length > 1) { @@ -532,8 +534,16 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { const rows = shouldComputeTotalItems || limit > 0 ? await dbQuery : []; - const totalItems = - cursor.totalItems ?? (rows.length ? Number(rows[0].count) : 0); + let totalItems: number; + if (cursor.totalItems !== undefined) { + totalItems = cursor.totalItems; + } else if (cursor.skipTotalItems) { + totalItems = 0; + } else if (rows.length) { + totalItems = Number(rows[0].count); + } else { + totalItems = 0; + } if (isFetchingBackwards) { rows.reverse(); @@ -809,15 +819,26 @@ export const cursorParser: z.ZodSchema = z.object({ function parseCursorFromRequest( request?: QueryEntitiesRequest, -): Partial { +): Partial & { skipTotalItems: boolean } { if (isQueryEntitiesInitialRequest(request)) { - const { filter, orderFields: sortFields = [], fullTextFilter } = request; - return { filter, orderFields: sortFields, fullTextFilter }; + const { + filter, + orderFields: sortFields = [], + fullTextFilter, + skipTotalItems = false, + } = request; + return { filter, orderFields: sortFields, fullTextFilter, skipTotalItems }; } if (isQueryEntitiesCursorRequest(request)) { - return request.cursor; + return { + ...request.cursor, + // Doesn't matter here + skipTotalItems: false, + }; } - return {}; + return { + skipTotalItems: false, + }; } function invertOrder(order: EntityOrder['order']) { diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 8c4414984e..0c1d16b525 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -173,6 +173,7 @@ describe('createRouter readonly disabled', () => { }, limit: 10000, credentials: mockCredentials.user(), + skipTotalItems: true, }); }); }); diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 196685337c..3a6dc82586 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -192,7 +192,14 @@ export async function createRouter( do { const result = await entitiesCatalog.queryEntities( !cursor - ? { credentials, fields, limit, filter, orderFields: order } + ? { + credentials, + fields, + limit, + filter, + orderFields: order, + skipTotalItems: true, + } : { credentials, fields, limit, cursor }, );