feat: add support for the querying in the catalog client

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2026-02-17 11:02:14 +01:00
parent 723b94aa16
commit e172faf7be
2 changed files with 90 additions and 0 deletions
@@ -683,6 +683,83 @@ describe('InMemoryCatalogClient', () => {
]);
});
it('filters by predicate query', async () => {
const client = new InMemoryCatalogClient({ entities });
const result = await client.queryEntities({
query: { kind: 'CustomKind' },
});
expect(result.items).toEqual([entity1, entity3]);
expect(result.totalItems).toBe(2);
});
it('filters by predicate query with $all', async () => {
const client = new InMemoryCatalogClient({ entities });
const result = await client.queryEntities({
query: {
$all: [{ kind: 'CustomKind' }, { 'spec.type': 'service' }],
},
});
expect(result.items).toEqual([entity1, entity3]);
});
it('filters by predicate query with $any', async () => {
const client = new InMemoryCatalogClient({ entities });
const result = await client.queryEntities({
query: {
$any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }],
},
});
expect(result.items).toEqual([entity1, entity3, entity4]);
});
it('filters by predicate query with $not', async () => {
const client = new InMemoryCatalogClient({ entities });
const result = await client.queryEntities({
query: {
$all: [
{ kind: 'CustomKind' },
{ $not: { 'spec.lifecycle': 'production' } },
],
},
});
expect(result.items).toEqual([]);
});
it('filters by predicate query with $in', async () => {
const client = new InMemoryCatalogClient({ entities });
const result = await client.queryEntities({
query: { 'spec.type': { $in: ['service', 'library'] } },
});
expect(result.items).toEqual([entity1, entity2, entity3]);
});
it('filters by predicate query with $exists', async () => {
const client = new InMemoryCatalogClient({ entities });
const result = await client.queryEntities({
query: { 'spec.lifecycle': { $exists: false } },
});
expect(result.items).toEqual([entity4]);
});
it('preserves query predicate through cursor pagination', async () => {
const client = new InMemoryCatalogClient({ entities });
const page1 = await client.queryEntities({
query: { kind: 'CustomKind' },
orderFields: { field: 'metadata.name', order: 'asc' },
limit: 1,
});
expect(page1.items.map(e => e.metadata.name)).toEqual(['e1']);
expect(page1.totalItems).toBe(2);
expect(page1.pageInfo.nextCursor).toBeDefined();
const page2 = await client.queryEntities({
cursor: page1.pageInfo.nextCursor!,
limit: 1,
});
expect(page2.items.map(e => e.metadata.name)).toEqual(['e3']);
expect(page2.pageInfo.nextCursor).toBeUndefined();
});
it('throws InputError for invalid cursor', async () => {
const client = new InMemoryCatalogClient({ entities });
await expect(
@@ -51,6 +51,7 @@ import {
NotFoundError,
NotImplementedError,
} from '@backstage/errors';
import { filterPredicateToFilterFunction } from '@backstage/filter-predicates';
import lodash from 'lodash';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch';
@@ -373,6 +374,7 @@ export class InMemoryCatalogClient implements CatalogApi {
): Promise<QueryEntitiesResponse> {
// Decode query parameters from cursor or from the request directly
let filter: EntityFilterQuery | undefined;
let query: Record<string, unknown> | undefined;
let orderFields: EntityOrderQuery | undefined;
let fullTextFilter: { term: string; fields?: string[] } | undefined;
let offset: number;
@@ -386,12 +388,17 @@ export class InMemoryCatalogClient implements CatalogApi {
throw new InputError('Invalid cursor');
}
filter = deserializeFilter(c.filter as any[]);
query = c.query as Record<string, unknown> | undefined;
orderFields = c.orderFields as EntityOrderQuery | undefined;
fullTextFilter = c.fullTextFilter as typeof fullTextFilter;
offset = c.offset as number;
limit = request.limit;
} else {
filter = request?.filter;
query =
request?.query && typeof request.query === 'object'
? (request.query as Record<string, unknown>)
: undefined;
orderFields = request?.orderFields;
fullTextFilter = request?.fullTextFilter;
offset = request?.offset ?? 0;
@@ -401,6 +408,11 @@ export class InMemoryCatalogClient implements CatalogApi {
// Apply filter
let items = this.#entities.filter(createFilter(filter));
// Apply predicate-based query filter
if (query) {
items = items.filter(filterPredicateToFilterFunction(query));
}
// Apply full-text filter, defaulting to the sort field or metadata.uid
if (fullTextFilter) {
const orderFieldsList = orderFields ? [orderFields].flat() : [];
@@ -432,6 +444,7 @@ export class InMemoryCatalogClient implements CatalogApi {
const cursorBase = {
filter: serializeFilter(filter),
query,
orderFields,
fullTextFilter,
totalItems,