refactor(catalog): use @backstage/filter-predicates package types

- Replace EntityPredicate with FilterPredicate
- update package.json and yarn.lock
- Add InputError handling for invalid predicate values in filter application
- Update catalog-client utils.ts to use browser-compatible atob() for base64 decoding

Signed-off-by: Nilay1999 <nilayparmar19@gmail.com>
This commit is contained in:
Nilay1999
2026-02-12 00:24:32 +05:30
committed by benjdlambert
parent 8ddce23115
commit 4bc4eb06a0
7 changed files with 42 additions and 98 deletions
+2 -1
View File
@@ -32,7 +32,8 @@ export function isQueryEntitiesInitialRequest(
*/
export function cursorContainsQuery(cursor: string): boolean {
try {
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString('utf8'));
// Use browser-compatible base64 decoding
const decoded = JSON.parse(atob(cursor));
return !!decoded.query;
} catch {
return false;
+5 -4
View File
@@ -16,7 +16,8 @@
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { EntityFilter, EntityPredicate } from '@backstage/plugin-catalog-node';
import { EntityFilter } from '@backstage/plugin-catalog-node';
import { FilterPredicate } from '@backstage/filter-predicates';
/**
* A pagination rule for entities.
@@ -53,7 +54,7 @@ export type EntitiesRequest = {
};
export type EntityPredicateRequest = {
query?: EntityPredicate;
query?: FilterPredicate;
order?: EntityOrder[];
pagination?: EntityPagination;
credentials: BackstageCredentials;
@@ -232,7 +233,7 @@ export interface QueryEntitiesInitialRequest {
* Predicate-based query for filtering entities.
* Mutually exclusive with filter.
*/
query?: EntityPredicate;
query?: FilterPredicate;
orderFields?: EntityOrder[];
fullTextFilter?: {
term: string;
@@ -298,7 +299,7 @@ export type Cursor = {
* A predicate-based query to be applied to the full list of entities.
* Mutually exclusive with filter.
*/
query?: EntityPredicate;
query?: FilterPredicate;
/**
* true if the cursor is a previous cursor.
*/
@@ -30,10 +30,8 @@ import {
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { InputError, serializeError } from '@backstage/errors';
import {
EntityPredicate,
LocationAnalyzer,
} from '@backstage/plugin-catalog-node';
import { parseFilterPredicate } from '@backstage/filter-predicates';
import { LocationAnalyzer } from '@backstage/plugin-catalog-node';
import express from 'express';
import yn from 'yn';
import { z } from 'zod';
@@ -272,7 +270,10 @@ export async function createRouter(
});
try {
const query = req.body.query as EntityPredicate | undefined;
// Validate the query using the Zod schema from @backstage/filter-predicates
const query = req.body.query
? parseFilterPredicate(req.body.query)
: undefined;
const order = parseEntityOrderFieldParams(req.query);
const pagination = parseEntityPaginationParams(req.query);
const credentials = await httpAuth.credentials(req);
@@ -15,32 +15,33 @@
*/
import {
EntityPredicate,
EntityPredicatePrimitive,
EntityPredicateValue,
} from '@backstage/plugin-catalog-node';
FilterPredicate,
FilterPredicatePrimitive,
FilterPredicateValue,
} from '@backstage/filter-predicates';
import { InputError } from '@backstage/errors';
import { Knex } from 'knex';
import { DbSearchRow } from '../../database/tables';
function isAllPredicate(
filter: EntityPredicate,
): filter is { $all: EntityPredicate[] } {
filter: FilterPredicate,
): filter is { $all: FilterPredicate[] } {
return typeof filter === 'object' && filter !== null && '$all' in filter;
}
function isAnyPredicate(
filter: EntityPredicate,
): filter is { $any: EntityPredicate[] } {
filter: FilterPredicate,
): filter is { $any: FilterPredicate[] } {
return typeof filter === 'object' && filter !== null && '$any' in filter;
}
function isNotPredicate(
filter: EntityPredicate,
): filter is { $not: EntityPredicate } {
filter: FilterPredicate,
): filter is { $not: FilterPredicate } {
return typeof filter === 'object' && filter !== null && '$not' in filter;
}
function isPrimitive(value: unknown): value is EntityPredicatePrimitive {
function isPrimitive(value: unknown): value is FilterPredicatePrimitive {
return (
typeof value === 'string' ||
typeof value === 'number' ||
@@ -49,18 +50,18 @@ function isPrimitive(value: unknown): value is EntityPredicatePrimitive {
}
function isExistsValue(
value: EntityPredicateValue,
value: FilterPredicateValue,
): value is { $exists: boolean } {
return typeof value === 'object' && value !== null && '$exists' in value;
}
function isInValue(
value: EntityPredicateValue,
): value is { $in: EntityPredicatePrimitive[] } {
value: FilterPredicateValue,
): value is { $in: FilterPredicatePrimitive[] } {
return typeof value === 'object' && value !== null && '$in' in value;
}
function isFieldExpression(filter: EntityPredicate): boolean {
function isFieldExpression(filter: FilterPredicate): boolean {
if (typeof filter !== 'object' || filter === null) {
return false;
}
@@ -109,7 +110,7 @@ function isFieldExpression(filter: EntityPredicate): boolean {
*/
function applyPredicateInStrategy(
filter: EntityPredicate,
filter: FilterPredicate,
targetQuery: Knex.QueryBuilder,
onEntityIdField: string,
knex: Knex,
@@ -211,6 +212,13 @@ function applyPredicateInStrategy(
value: String(value).toLowerCase(),
});
this.andWhere(onEntityIdField, 'in', matchQuery);
} else {
// Reject unsupported/invalid predicate values
throw new InputError(
`Invalid filter predicate value for field "${key}": expected a primitive value, $exists, or $in operator, but got ${JSON.stringify(
value,
)}`,
);
}
}
},
@@ -221,7 +229,7 @@ function applyPredicateInStrategy(
}
export function applyPredicateEntityFilterToQuery(options: {
filter: EntityPredicate;
filter: FilterPredicate;
targetQuery: Knex.QueryBuilder;
onEntityIdField: string;
knex: Knex;
+4 -1
View File
@@ -15,6 +15,7 @@
*/
import { InputError, NotAllowedError } from '@backstage/errors';
import { createZodV3FilterPredicateSchema } from '@backstage/filter-predicates';
import { Request } from 'express';
import lodash from 'lodash';
import { z } from 'zod';
@@ -109,6 +110,8 @@ const entityFilterParser: z.ZodSchema<EntityFilter> = z.lazy(() =>
.or(z.object({ allOf: z.array(entityFilterParser) })),
);
const filterPredicateSchema = createZodV3FilterPredicateSchema(z);
export const cursorParser: z.ZodSchema<Cursor> = z.object({
orderFields: z.array(
z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }),
@@ -122,7 +125,7 @@ export const cursorParser: z.ZodSchema<Cursor> = z.object({
orderFieldValues: z.array(z.string().or(z.null())),
filter: entityFilterParser.optional(),
isPrevious: z.boolean(),
query: z.any().optional(),
query: filterPredicateSchema.optional(),
firstSortFieldValues: z.array(z.string().or(z.null())).optional(),
totalItems: z.number().optional(),
});
-66
View File
@@ -85,69 +85,3 @@ export type EntitiesSearchFilter = {
*/
values?: string[];
};
/**
* Defines a predicate used to filter entities using logical and comparison operators.
*
* This type supports:
* - Primitive equality matching
* - Field existence checks
* - Inclusion checks
* - Logical composition using $all, $any, and $not
*
* @public
*/
export type EntityPredicate =
| EntityPredicateExpression
| EntityPredicatePrimitive
| { $all: EntityPredicate[] }
| { $any: EntityPredicate[] }
| { $not: EntityPredicate };
/**
* Represents a field-based predicate expression.
*
* Each key is treated as a JSON path into the entity object.
* Keys starting with `$` are reserved for operators and are not allowed.
*
* Example:
* ```ts
* {
* "spec.type": "service",
* "metadata.name": { $in: ["payments", "orders"] }
* }
* ```
*
* @public
*/
export type EntityPredicateExpression = {
/** JSON path of the entity field */
[KPath in string]: EntityPredicateValue;
} & {
/** Operator keys are not allowed as field paths */
[KPath in `$${string}`]: never;
};
/**
* Represents a value condition for a predicate field.
*
* Supported forms:
* - Primitive equality match
* - Existence check using `$exists`
* - Inclusion check using `$in`
*
* @public
*/
export type EntityPredicateValue =
| EntityPredicatePrimitive
| { $exists: boolean }
| { $in: EntityPredicatePrimitive[] };
/**
* Represents a primitive predicate value.
*
* Used for direct equality comparison.
*
* @public
*/
export type EntityPredicatePrimitive = string | number | boolean;
-4
View File
@@ -20,10 +20,6 @@ export type {
LocationSpec,
EntitiesSearchFilter,
EntityFilter,
EntityPredicate,
EntityPredicateExpression,
EntityPredicateValue,
EntityPredicatePrimitive,
} from './common';
export type {
CatalogProcessor,