From 875ceaee9a016b2af1dac949bf4735623fcfd535 Mon Sep 17 00:00:00 2001 From: Jordan Snow Date: Thu, 29 Aug 2024 17:16:36 +0200 Subject: [PATCH] Negation key word to entity filter Signed-off-by: Jordan Snow --- .../alpha/filter/matchers/createHasMatcher.ts | 5 +++- .../alpha/filter/matchers/createIsMatcher.ts | 4 ++- .../filter/matchers/createKindMatcher.ts | 5 +++- .../filter/matchers/createTypeMatcher.ts | 7 ++--- .../filter/parseFilterExpression.test.ts | 14 ++++++++++ .../src/alpha/filter/parseFilterExpression.ts | 27 ++++++++++++++++--- 6 files changed, 52 insertions(+), 10 deletions(-) diff --git a/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts index 9fd393a561..a34834e40d 100644 --- a/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts +++ b/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts @@ -16,6 +16,7 @@ import { InputError } from '@backstage/errors'; import { EntityMatcherFn } from './types'; +import { Entity } from '@backstage/catalog-model'; const allowedMatchers: Record = { labels: entity => { @@ -32,6 +33,7 @@ const allowedMatchers: Record = { export function createHasMatcher( parameters: string[], onParseError: (error: Error) => void, + negation?: boolean, ): EntityMatcherFn { const matchers = parameters.flatMap(parameter => { const matcher = allowedMatchers[parameter.toLocaleLowerCase('en-US')]; @@ -47,6 +49,7 @@ export function createHasMatcher( return [matcher]; }); - return entity => + const isMatch = (entity: Entity) => matchers.length ? matchers.some(matcher => matcher(entity)) : true; + return negation ? entity => !isMatch(entity) : isMatch; } diff --git a/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts index 49a54349d1..5df4f2ffe8 100644 --- a/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts +++ b/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts @@ -28,6 +28,7 @@ const allowedMatchers: Record = { export function createIsMatcher( parameters: string[], onParseError: (error: Error) => void, + negation?: boolean, ): EntityMatcherFn { const matchers = parameters.flatMap(parameter => { const matcher = allowedMatchers[parameter.toLocaleLowerCase('en-US')]; @@ -43,6 +44,7 @@ export function createIsMatcher( return [matcher]; }); - return entity => + const isMatch = (entity: any) => matchers.length ? matchers.some(matcher => matcher(entity)) : true; + return negation ? entity => !isMatch(entity) : isMatch; } diff --git a/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts index 4f7c475ebe..6d7a3ff162 100644 --- a/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts +++ b/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts @@ -22,7 +22,10 @@ import { EntityMatcherFn } from './types'; export function createKindMatcher( parameters: string[], _onParseError: (error: Error) => void, + negation?: boolean, ): EntityMatcherFn { const items = parameters.map(p => p.toLocaleLowerCase('en-US')); - return entity => items.includes(entity.kind.toLocaleLowerCase('en-US')); + const isMatch = (entity: any) => + items.includes(entity.kind.toLocaleLowerCase('en-US')); + return negation ? entity => !isMatch(entity) : isMatch; } diff --git a/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts index 31f7a5a461..7bd7e56dfe 100644 --- a/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts +++ b/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts @@ -22,13 +22,14 @@ import { EntityMatcherFn } from './types'; export function createTypeMatcher( parameters: string[], _onParseError: (error: Error) => void, + negation?: boolean, ): EntityMatcherFn { const items = parameters.map(p => p.toLocaleLowerCase('en-US')); return entity => { const value = entity.spec?.type; - return ( + const isMatch = typeof value === 'string' && - items.includes(value.toLocaleLowerCase('en-US')) - ); + items.includes(value.toLocaleLowerCase('en-US')); + return negation ? !isMatch : isMatch; }; } diff --git a/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts b/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts index 616091739e..7ebadde13a 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'"`, diff --git a/plugins/catalog/src/alpha/filter/parseFilterExpression.ts b/plugins/catalog/src/alpha/filter/parseFilterExpression.ts index 4f809690f7..906fa45367 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,10 +61,14 @@ export function parseFilterExpression(expression: string): { const parts = splitFilterExpression(expression, e => expressionParseErrors.push(e), ); - + let negation = false; const matchers = parts.flatMap(part => { const factory = rootMatcherFactories[part.key]; if (!factory) { + if (isNegation(part.key)) { + negation = true; + return []; + } const known = Object.keys(rootMatcherFactories).map(m => `'${m}'`); expressionParseErrors.push( new InputError( @@ -73,9 +78,12 @@ export function parseFilterExpression(expression: string): { return []; } - const matcher = factory(part.parameters, e => - expressionParseErrors.push(e), + const matcher = factory( + part.parameters, + e => expressionParseErrors.push(e), + negation, ); + negation = false; return [matcher]; }); @@ -106,7 +114,11 @@ export function splitFilterExpression( const result = new Array<{ key: string; parameters: string[] }>(); for (const word of words) { - const match = word.match(/^([^:]+):(.+)$/); + let match = word.match(/^([^:]+):(.+)$/); + if (match && isNegation(match[1])) { + result.push({ key: 'not', parameters: [] }); + match = match[2].match(/^([^:]+):(.+)$/) || null; + } if (!match) { onParseError( new InputError( @@ -124,3 +136,10 @@ export function splitFilterExpression( return result; } + +function isNegation(parameter: string): boolean { + if (parameter.toLocaleLowerCase('en-US') === 'not') { + return true; + } + return false; +}