diff --git a/.changeset/green-cooks-sort.md b/.changeset/green-cooks-sort.md new file mode 100644 index 0000000000000..51dd86ac2e567 --- /dev/null +++ b/.changeset/green-cooks-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Adding negation keyword for entity filtering diff --git a/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts b/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts index 616091739e1a3..eda5def76ede6 100644 --- a/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts +++ b/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts @@ -93,6 +93,20 @@ describe('parseFilterExpression', () => { ); }); + it('recognizes negation key', () => { + const component = { kind: 'Component' } as unknown as Entity; + expect(run('not:kind:user')(component)).toBe(true); + }); + + it('supports negation and affirmative expressions', () => { + const component = { + kind: 'Component', + spec: { type: 'service' }, + } as unknown as Entity; + expect(run('not:kind:user type:service')(component)).toBe(true); + expect(run('type:service not:kind:user')(component)).toBe(true); + }); + it('rejects unknown keys', () => { expect(() => run('unknown:foo')).toThrowErrorMatchingInlineSnapshot( `"'unknown' is not a valid filter expression key, expected one of 'kind','type','is','has'"`, @@ -123,17 +137,25 @@ describe('splitFilterExpression', () => { expect(run('')).toEqual([]); expect(run(' ')).toEqual([]); expect(run('kind:component')).toEqual([ - { key: 'kind', parameters: ['component'] }, + { key: 'kind', parameters: ['component'], negation: false }, ]); expect(run('kind:component,user')).toEqual([ - { key: 'kind', parameters: ['component', 'user'] }, + { key: 'kind', parameters: ['component', 'user'], negation: false }, + ]); + expect(run('kind:component,user not:type:foo')).toEqual([ + { key: 'kind', parameters: ['component', 'user'], negation: false }, + { key: 'type', parameters: ['foo'], negation: true }, + ]); + expect(run('not:type:foo kind:component,user')).toEqual([ + { key: 'type', parameters: ['foo'], negation: true }, + { key: 'kind', parameters: ['component', 'user'], negation: false }, ]); expect(run('kind:component,user type:foo')).toEqual([ - { key: 'kind', parameters: ['component', 'user'] }, - { key: 'type', parameters: ['foo'] }, + { key: 'kind', parameters: ['component', 'user'], negation: false }, + { key: 'type', parameters: ['foo'], negation: false }, ]); expect(run('with:multiple:colons')).toEqual([ - { key: 'with', parameters: ['multiple:colons'] }, + { key: 'with', parameters: ['multiple:colons'], negation: false }, ]); }); diff --git a/plugins/catalog/src/alpha/filter/parseFilterExpression.ts b/plugins/catalog/src/alpha/filter/parseFilterExpression.ts index 4f809690f713c..9775f16e51e03 100644 --- a/plugins/catalog/src/alpha/filter/parseFilterExpression.ts +++ b/plugins/catalog/src/alpha/filter/parseFilterExpression.ts @@ -27,6 +27,7 @@ const rootMatcherFactories: Record< ( parameters: string[], onParseError: (error: Error) => void, + negation?: boolean, ) => EntityMatcherFn > = { kind: createKindMatcher, @@ -60,9 +61,9 @@ export function parseFilterExpression(expression: string): { const parts = splitFilterExpression(expression, e => expressionParseErrors.push(e), ); - const matchers = parts.flatMap(part => { const factory = rootMatcherFactories[part.key]; + const negation = part.negation; if (!factory) { const known = Object.keys(rootMatcherFactories).map(m => `'${m}'`); expressionParseErrors.push( @@ -76,7 +77,8 @@ export function parseFilterExpression(expression: string): { const matcher = factory(part.parameters, e => expressionParseErrors.push(e), ); - return [matcher]; + + return [negation ? (entity: Entity) => !matcher(entity) : matcher]; }); const filterFn = (entity: Entity) => @@ -97,16 +99,20 @@ export function parseFilterExpression(expression: string): { export function splitFilterExpression( expression: string, onParseError: (error: Error) => void, -): Array<{ key: string; parameters: string[] }> { +): Array<{ key: string; parameters: string[]; negation: boolean }> { const words = expression .split(' ') .map(w => w.trim()) .filter(Boolean); - const result = new Array<{ key: string; parameters: string[] }>(); + const result = new Array<{ + key: string; + parameters: string[]; + negation: boolean; + }>(); for (const word of words) { - const match = word.match(/^([^:]+):(.+)$/); + const match = word.match(/^(not:)?([^:]+):(.+)$/); if (!match) { onParseError( new InputError( @@ -115,11 +121,10 @@ export function splitFilterExpression( ); continue; } - - const key = match[1]; - const parameters = match[2].split(',').filter(Boolean); // silently ignore double commas - - result.push({ key, parameters }); + const key = match[2]; + const parameters = match[3].split(',').filter(Boolean); // silently ignore double commas + const negation = Boolean(match[1]); + result.push({ key, parameters, negation }); } return result;