diff --git a/.changeset/slimy-days-leave.md b/.changeset/slimy-days-leave.md index 250bf32962..df772d80d4 100644 --- a/.changeset/slimy-days-leave.md +++ b/.changeset/slimy-days-leave.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': patch --- -Allow nested EntityFilters +Allow singleton and flexibly nested EntityFilters diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index e83fa68cad..143e31bc21 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -870,11 +870,14 @@ export type EntityAncestryResponse = { // Warning: (ae-missing-release-tag) "EntityFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type EntityFilter = { - anyOf: { - allOf: (EntitiesSearchFilter | EntityFilter)[]; - }[]; -}; +export type EntityFilter = + | { + allOf: EntityFilter[]; + } + | { + anyOf: EntityFilter[]; + } + | EntitiesSearchFilter; // Warning: (ae-missing-release-tag) "EntityPagination" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index b0b06c563c..df693bfd42 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -22,9 +22,10 @@ import { Entity, EntityRelationSpec } from '@backstage/catalog-model'; * Any (at least one) of the outer sets must match, within which all of the * individual filters must match. */ -export type EntityFilter = { - anyOf: { allOf: (EntitiesSearchFilter | EntityFilter)[] }[]; -}; +export type EntityFilter = + | { allOf: EntityFilter[] } + | { anyOf: EntityFilter[] } + | EntitiesSearchFilter; /** * A pagination rule for entities. diff --git a/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts b/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts index 81b0612989..6a150f7583 100644 --- a/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts @@ -46,11 +46,11 @@ import { DbPageInfo, Transaction, } from './types'; -import { - EntityPagination, - EntityFilter, - EntitiesSearchFilter, -} from '../../catalog/types'; +import { EntityPagination, EntitiesSearchFilter } from '../../catalog/types'; + +type LegacyEntityFilter = { + anyOf: { allOf: EntitiesSearchFilter[] }[]; +}; // The number of items that are sent per batch to the database layer, when // doing .batchInsert calls to knex. This needs to be low enough to not cause @@ -221,10 +221,23 @@ export class CommonDatabase implements Database { let entitiesQuery = tx('entities'); - for (const singleFilter of request?.filter?.anyOf ?? []) { + if ( + request?.filter && + (request.filter.hasOwnProperty('key') || + request.filter.hasOwnProperty('allOf')) + ) { + throw new Error( + 'Filters for the legacy CommonDatabase must obey the { anyOf: [{ allOf: [] }] } format.', + ); + } + for (const singleFilter of (request?.filter as LegacyEntityFilter)?.anyOf ?? + []) { entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() { for (const filter of singleFilter.allOf) { - if (isEntityFilter(filter)) { + if ( + filter.hasOwnProperty('anyOf') || + filter.hasOwnProperty('allOf') + ) { throw new Error( 'Nested filters are not supported in the legacy CommonDatabase', ); @@ -612,9 +625,3 @@ function deduplicateRelations( r => `${r.source_full_name}:${r.target_full_name}:${r.type}`, ); } - -function isEntityFilter( - filter: EntitiesSearchFilter | EntityFilter, -): filter is EntityFilter { - return filter.hasOwnProperty('anyOf'); -} diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts index 0c805e1b17..32d5fa843e 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts @@ -284,9 +284,7 @@ describe('NextEntitiesCatalog', () => { key: 'spec.test', matchValueExists: true, }; - const request = { - filter: { anyOf: [{ allOf: [testFilter] }] }, - }; + const request = { filter: testFilter }; const { entities } = await catalog.entities(request); expect(entities.length).toBe(1); @@ -344,14 +342,10 @@ describe('NextEntitiesCatalog', () => { }; const request = { filter: { - anyOf: [ + allOf: [ + testFilter1, { - allOf: [ - testFilter1, - { - anyOf: [{ allOf: [testFilter2] }, { allOf: [testFilter3] }], - }, - ], + anyOf: [testFilter2, testFilter3], }, ], }, diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts index 52d6ed1bdd..841e57c921 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts @@ -107,30 +107,56 @@ function addCondition( ); } -function isEntityFilter( +function isEntitiesSearchFilter( filter: EntitiesSearchFilter | EntityFilter, -): filter is EntityFilter { +): filter is EntitiesSearchFilter { + return filter.hasOwnProperty('key'); +} + +function isAndEntityFilter( + filter: { allOf: EntityFilter[] } | EntityFilter, +): filter is { allOf: EntityFilter[] } { + return filter.hasOwnProperty('allOf'); +} + +function isOrEntityFilter( + filter: { anyOf: EntityFilter[] } | EntityFilter, +): filter is { anyOf: EntityFilter[] } { return filter.hasOwnProperty('anyOf'); } function parseFilter( - filters: EntityFilter, + filter: EntityFilter, query: Knex.QueryBuilder, db: Knex, ): Knex.QueryBuilder { - let cumulativeQuery = query; - for (const singleFilter of filters?.anyOf ?? []) { - cumulativeQuery = cumulativeQuery.orWhere(function singleFilterFn() { - for (const filter of singleFilter.allOf) { - if (isEntityFilter(filter)) { - this.andWhere(subQuery => parseFilter(filter, subQuery, db)); - } else { - addCondition(this, db, filter); - } - } + if (isEntitiesSearchFilter(filter)) { + return query.where(function filterFunction() { + addCondition(this, db, filter); }); } - return cumulativeQuery; + + if (isOrEntityFilter(filter)) { + let cumulativeQuery = query; + for (const subFilter of filter.anyOf ?? []) { + cumulativeQuery = cumulativeQuery.orWhere(subQuery => + parseFilter(subFilter, subQuery, db), + ); + } + return cumulativeQuery; + } + + if (isAndEntityFilter(filter)) { + let cumulativeQuery = query; + for (const subFilter of filter.allOf ?? []) { + cumulativeQuery = cumulativeQuery.andWhere(subQuery => + parseFilter(subFilter, subQuery, db), + ); + } + return cumulativeQuery; + } + + return query; } export class NextEntitiesCatalog implements EntitiesCatalog {