@@ -38,6 +38,9 @@ import { DefaultCheckRegistry } from './CheckRegistry';
3838import { readChecksFromConfig } from './config' ;
3939import * as validationSchema from './validation-schema.json' ;
4040import { LoggerService } from '@backstage/backend-plugin-api' ;
41+ import { CatalogApi } from '@backstage/catalog-client' ;
42+ import { Entity } from '@backstage/catalog-model' ;
43+ import { get } from 'lodash' ;
4144
4245const noopEvent = {
4346 type : 'noop' ,
@@ -53,8 +56,9 @@ export type JsonRulesEngineFactCheckerOptions = {
5356 checks : TechInsightJsonRuleCheck [ ] ;
5457 repository : TechInsightsStore ;
5558 logger : LoggerService ;
56- checkRegistry ?: TechInsightCheckRegistry < any > ;
59+ checkRegistry ?: TechInsightCheckRegistry < TechInsightJsonRuleCheck > ;
5760 operators ?: Operator [ ] ;
61+ catalogApi ?: CatalogApi ;
5862} ;
5963
6064/**
@@ -71,13 +75,16 @@ export class JsonRulesEngineFactChecker
7175 private readonly logger : LoggerService ;
7276 private readonly validationSchema : SchemaObject ;
7377 private readonly operators : Operator [ ] ;
78+ private readonly catalogApi ?: CatalogApi ;
7479
7580 constructor ( options : JsonRulesEngineFactCheckerOptions ) {
76- const { checks, repository, logger, checkRegistry, operators } = options ;
81+ const { checks, repository, logger, checkRegistry, operators, catalogApi } =
82+ options ;
7783
7884 this . repository = repository ;
7985 this . logger = logger ;
8086 this . operators = operators || [ ] ;
87+ this . catalogApi = catalogApi ;
8188 this . validationSchema = JSON . parse ( JSON . stringify ( validationSchema ) ) ;
8289
8390 this . operators . forEach ( op => {
@@ -92,6 +99,125 @@ export class JsonRulesEngineFactChecker
9299 new DefaultCheckRegistry < TechInsightJsonRuleCheck > ( checks ) ;
93100 }
94101
102+ /**
103+ * Evaluates whether an entity matches the given filter criteria.
104+ * Supports both single filter objects and arrays of filter objects.
105+ * When multiple filter objects are provided, uses OR logic (entity matches if ANY filter matches).
106+ *
107+ * @param entity - The catalog entity to evaluate
108+ * @param filter - Single filter object or array of filter objects to match against
109+ * @returns true if the entity matches the filter criteria, false otherwise
110+ */
111+ private matchesFilter (
112+ entity : Entity ,
113+ filter :
114+ | Record < string , string | symbol | ( string | symbol ) [ ] >
115+ | Record < string , string | symbol | ( string | symbol ) [ ] > [ ] ,
116+ ) : boolean {
117+ const filters = Array . isArray ( filter ) ? filter : [ filter ] ;
118+
119+ // Match if ANY of the filters match (OR logic between filter objects)
120+ return filters . some ( f => this . matchesSingleFilter ( entity , f ) ) ;
121+ }
122+
123+ /**
124+ * Evaluates whether an entity matches a single filter object.
125+ * All key-value pairs in the filter must match (AND logic).
126+ * Supports nested property access using lodash.get (e.g., "metadata.name").
127+ *
128+ * @param entity - The catalog entity to evaluate
129+ * @param filter - Filter object with key-value pairs that must all match
130+ * @returns true if all filter conditions match, false otherwise
131+ */
132+ private matchesSingleFilter (
133+ entity : Entity ,
134+ filter : Record < string , string | symbol | ( string | symbol ) [ ] > ,
135+ ) : boolean {
136+ // All conditions in a single filter must match (AND logic within a filter)
137+ return Object . entries ( filter ) . every ( ( [ key , value ] ) => {
138+ const entityValue = get ( entity , key ) ;
139+
140+ // Handle undefined/null entity values - if the property doesn't exist on the entity,
141+ // the filter condition cannot be satisfied, so return false
142+ if ( entityValue === undefined || entityValue === null ) {
143+ this . logger . warn ( `Entity property '${ key } ' is undefined or null` ) ;
144+ return false ;
145+ }
146+
147+ // Handle array values (OR logic within array)
148+ // If the filter value is an array, the entity matches if ANY value in the array matches
149+ if ( Array . isArray ( value ) ) {
150+ return value . some ( v => this . compareValues ( entityValue , v ) ) ;
151+ }
152+
153+ // Single value comparison
154+ return this . compareValues ( entityValue , value ) ;
155+ } ) ;
156+ }
157+
158+ /**
159+ * Helper method to compare entity property values against filter values.
160+ * Implements case-insensitive string comparison for better user experience,
161+ * as entity kinds, types, and lifecycles are typically case-insensitive.
162+ *
163+ * @param entityValue - The actual value from the entity property
164+ * @param filterValue - The expected value from the filter condition
165+ * @returns true if values match according to the comparison rules, false otherwise
166+ */
167+ private compareValues (
168+ entityValue : any ,
169+ filterValue : string | symbol ,
170+ ) : boolean {
171+ // Handle string comparison case-insensitively for kind, type, lifecycle, etc.
172+ // This provides a better user experience as these values are typically case-insensitive
173+ if ( typeof entityValue === 'string' && typeof filterValue === 'string' ) {
174+ return entityValue . toLowerCase ( ) === filterValue . toLowerCase ( ) ;
175+ }
176+
177+ // Handle symbol comparison (less common but supported for completeness)
178+ if ( typeof filterValue === 'symbol' ) {
179+ return entityValue === filterValue ;
180+ }
181+
182+ // Default to strict equality for all other types (numbers, booleans, etc.)
183+ return entityValue === filterValue ;
184+ }
185+
186+ /**
187+ * Fetches an entity from the catalog to enable filter evaluation.
188+ * This is required to access entity metadata (kind, type, lifecycle, etc.) for filtering.
189+ *
190+ * @param entityRef - The entity reference string (e.g., "component:default/my-service")
191+ * @returns The entity object if found and catalogApi is available, undefined otherwise
192+ */
193+ private async fetchEntityFromCatalog (
194+ entityRef : string ,
195+ ) : Promise < Entity | undefined > {
196+ // If catalogApi wasn't provided in the constructor, filtering cannot be performed
197+ if ( ! this . catalogApi ) {
198+ this . logger . debug (
199+ 'CatalogApi not available, skipping entity fetch for filtering' ,
200+ ) ;
201+ return undefined ;
202+ }
203+
204+ try {
205+ const entity = await this . catalogApi . getEntityByRef ( entityRef ) ;
206+
207+ if ( ! entity ) {
208+ this . logger . warn ( `Entity '${ entityRef } ' not found in catalog` ) ;
209+ }
210+
211+ return entity ;
212+ } catch ( e ) {
213+ // Log but don't throw - we'll fall back to running all checks without filtering
214+ this . logger . warn (
215+ `Failed to fetch entity ${ entityRef } from catalog: ${ e } ` ,
216+ ) ;
217+ return undefined ;
218+ }
219+ }
220+
95221 async runChecks (
96222 entity : string ,
97223 checks ?: string [ ] ,
@@ -104,6 +230,51 @@ export class JsonRulesEngineFactChecker
104230 const techInsightChecks = checks
105231 ? await this . checkRegistry . getAll ( checks )
106232 : await this . checkRegistry . list ( ) ;
233+
234+ // Identify checks that have filter criteria defined
235+ // Only these checks require entity fetching from the catalog
236+ const checksWithFilters = techInsightChecks . filter ( check => check . filter ) ;
237+
238+ // Start with all checks; will be filtered down if entity filtering is applicable
239+ let filteredChecks = techInsightChecks ;
240+
241+ // Only fetch entity from catalog if there are checks with filter criteria
242+ // This optimization avoids unnecessary catalog API calls
243+ if ( checksWithFilters . length > 0 ) {
244+ const catalogEntity = await this . fetchEntityFromCatalog ( entity ) ;
245+ if ( catalogEntity ) {
246+ const initialCount = filteredChecks . length ;
247+
248+ // Apply filter criteria to determine which checks should run for this entity
249+ // Checks without filters always run; checks with filters only run if they match
250+ filteredChecks = filteredChecks . filter ( check => {
251+ // Always include checks that don't have filter criteria
252+ if ( ! check . filter ) {
253+ return true ;
254+ }
255+
256+ // Evaluate if the entity matches the check's filter criteria
257+ const matches = this . matchesFilter ( catalogEntity , check . filter ) ;
258+ return matches ;
259+ } ) ;
260+
261+ // Log how many checks were filtered out for observability
262+ // This helps users understand why certain checks didn't run
263+ const skippedCount = initialCount - filteredChecks . length ;
264+ if ( skippedCount > 0 ) {
265+ this . logger . info (
266+ `Filtered out ${ skippedCount } check(s) based on entity criteria` ,
267+ ) ;
268+ }
269+ } else {
270+ // If we couldn't fetch the entity, run all checks as a fallback
271+ // This ensures checks still run even if catalog is unavailable
272+ this . logger . warn (
273+ 'Could not fetch entity from catalog for filtering - running all checks' ,
274+ ) ;
275+ }
276+ }
277+
107278 const factRetrieversIds = techInsightChecks . flatMap ( it => it . factIds ) ;
108279 const facts = await this . repository . getLatestFactsByIds (
109280 factRetrieversIds ,
@@ -380,6 +551,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = {
380551 logger : LoggerService ;
381552 checkRegistry ?: TechInsightCheckRegistry < TechInsightJsonRuleCheck > ;
382553 operators ?: Operator [ ] ;
554+ catalogApi ?: CatalogApi ;
383555} ;
384556
385557/**
@@ -394,6 +566,7 @@ export class JsonRulesEngineFactCheckerFactory {
394566 private readonly logger : LoggerService ;
395567 private readonly checkRegistry ?: TechInsightCheckRegistry < TechInsightJsonRuleCheck > ;
396568 private readonly operators ?: Operator [ ] ;
569+ private readonly catalogApi ?: CatalogApi ;
397570
398571 static fromConfig (
399572 config : Config ,
@@ -412,6 +585,7 @@ export class JsonRulesEngineFactCheckerFactory {
412585 this . checks = options . checks ;
413586 this . checkRegistry = options . checkRegistry ;
414587 this . operators = options . operators ;
588+ this . catalogApi = options . catalogApi ;
415589 }
416590
417591 /**
@@ -426,6 +600,7 @@ export class JsonRulesEngineFactCheckerFactory {
426600 checkRegistry : this . checkRegistry ,
427601 repository,
428602 operators : this . operators ,
603+ catalogApi : this . catalogApi ,
429604 } ) ;
430605 }
431606}
0 commit comments