diff --git a/.changeset/fifty-grapes-explode.md b/.changeset/fifty-grapes-explode.md new file mode 100644 index 0000000000..999a105c4f --- /dev/null +++ b/.changeset/fifty-grapes-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests. diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 08ae295834..93a37fb04a 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -1418,6 +1418,87 @@ describe('DefaultEntitiesCatalog', () => { }, ); + it.each(databases.eachSupportedId())( + 'should exclude filtered entities when paginating, %p', + async databaseId => { + await createDatabase(databaseId); + + await Promise.all([ + addEntityToSearch(entityFrom('AA', { uid: '1', kind: 'included' })), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace2', + kind: 'included', + uid: '2', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'ns', + kind: 'excluded', + uid: '3', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace3', + uid: '4', + kind: 'included', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace4', + uid: '5', + kind: 'included', + }), + ), + addEntityToSearch(entityFrom('CC', { uid: '6', kind: 'included' })), + addEntityToSearch(entityFrom('DD', { uid: '7', kind: 'included' })), + ]); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const limit = 2; + + // initial request + const request1: QueryEntitiesInitialRequest = { + limit, + filter: { + key: 'kind', + values: ['included'], + }, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + }; + const response1 = await catalog.queryEntities(request1); + expect(response1.items).toMatchObject([ + entityFrom('AA', { uid: '1', kind: 'included' }), + entityFrom('AA', { uid: '2', kind: 'included' }), + ]); + expect(response1.pageInfo.nextCursor).toBeDefined(); + expect(response1.pageInfo.prevCursor).toBeUndefined(); + expect(response1.totalItems).toBe(6); + + // second request (forward) + const request2: QueryEntitiesCursorRequest = { + cursor: response1.pageInfo.nextCursor!, + limit, + }; + const response2 = await catalog.queryEntities(request2); + expect(response2.items).toMatchObject([ + entityFrom('AA', { uid: '4', kind: 'included' }), + entityFrom('AA', { uid: '5', kind: 'included' }), + ]); + expect(response2.pageInfo.nextCursor).toBeDefined(); + expect(response2.pageInfo.prevCursor).toBeDefined(); + expect(response2.totalItems).toBe(6); + }, + ); + it.each(databases.eachSupportedId())( 'should paginate results without sort fields, %p', async databaseId => { @@ -1754,11 +1835,12 @@ function entityFrom( uid, namespace, title, - }: { uid?: string; namespace?: string; title?: string } = {}, + kind = 'k', + }: { uid?: string; namespace?: string; title?: string; kind?: string } = {}, ) { return { apiVersion: 'a', - kind: 'k', + kind, metadata: { name, ...(!!namespace && { namespace }), diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index ba165f96af..5fca950cac 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -413,17 +413,18 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { const isOrderingDescending = sortField.order === 'desc'; if (prevItemOrderFieldValue) { - dbQuery.andWhere( - 'value', - isFetchingBackwards !== isOrderingDescending ? '<' : '>', - prevItemOrderFieldValue, - ); - dbQuery.orWhere(function nested() { - this.where('value', '=', prevItemOrderFieldValue).andWhere( - 'search.entity_id', + dbQuery.andWhere(function nested() { + this.where( + 'value', isFetchingBackwards !== isOrderingDescending ? '<' : '>', - prevItemUid, - ); + prevItemOrderFieldValue, + ) + .orWhere('value', '=', prevItemOrderFieldValue) + .andWhere( + 'search.entity_id', + isFetchingBackwards !== isOrderingDescending ? '<' : '>', + prevItemUid, + ); }); }