diff --git a/.changeset/slow-singers-jump.md b/.changeset/slow-singers-jump.md new file mode 100644 index 0000000000..2eef032d7b --- /dev/null +++ b/.changeset/slow-singers-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Fetch all facets in a single database query diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 91406a9b08..08ba11c59d 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -2060,6 +2060,50 @@ describe('DefaultEntitiesCatalog', () => { }); }, ); + + it.each(databases.eachSupportedId())( + 'works with a mixture of present and missing facets, %p', + async databaseId => { + await createDatabase(databaseId); + + await addEntityToSearch({ + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'one', + }, + spec: {}, + }); + await addEntityToSearch({ + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'two', + }, + spec: {}, + }); + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: mockServices.logger.mock(), + stitcher, + }); + + await expect( + catalog.facets({ + facets: ['metadata.name', 'missing'], + credentials: mockCredentials.none(), + }), + ).resolves.toEqual({ + facets: { + 'metadata.name': expect.arrayContaining([ + { value: 'one', count: 1 }, + { value: 'two', count: 1 }, + ]), + missing: [], + }, + }); + }, + ); }); }); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index e4b4fb001d..74d08d3ad7 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -720,26 +720,40 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { } async facets(request: EntityFacetsRequest): Promise { + const query = this.database('search') + .whereIn( + 'search.key', + request.facets.map(f => f.toLocaleLowerCase('en-US')), + ) + .whereNotNull('search.original_value') + .select({ + facet: 'search.key', + value: 'search.original_value', + count: this.database.raw('count(*)'), + }) + .groupBy(['search.key', 'search.original_value']); + + if (request.filter) { + parseFilter( + request.filter, + query, + this.database, + false, + 'search.entity_id', + ); + } + + const rows = await query; + const facets: EntityFacetsResponse['facets'] = {}; - const db = this.database; - for (const facet of request.facets) { - const dbQuery = db('search') - .where('search.key', facet.toLocaleLowerCase('en-US')) - .whereNotNull('search.original_value') - .select({ value: 'search.original_value', count: db.raw('count(*)') }) - .groupBy('search.original_value'); - - if (request?.filter) { - parseFilter(request.filter, dbQuery, db, false, 'search.entity_id'); - } - - const result = await dbQuery; - - facets[facet] = result.map(data => ({ - value: String(data.value), - count: Number(data.count), - })); + const facetLowercase = facet.toLocaleLowerCase('en-US'); + facets[facet] = rows + .filter(row => row.facet === facetLowercase) + .map(row => ({ + value: String(row.value), + count: Number(row.count), + })); } return { facets };