From 4a47c603e1ec2eedbe26821be292a30f41e66c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 21 Feb 2026 16:10:40 +0100 Subject: [PATCH] add docs too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/features/software-catalog/api.md | 187 +++++++++++++++++++++++ packages/catalog-client/report.api.md | 2 +- packages/catalog-client/src/types/api.ts | 10 +- 3 files changed, 194 insertions(+), 5 deletions(-) diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md index ca8b8f638d..3cd70538c2 100644 --- a/docs/features/software-catalog/api.md +++ b/docs/features/software-catalog/api.md @@ -210,6 +210,193 @@ if `prevCursor` exists, it can be used to retrieve the previous batch of entitie it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. +### `POST /entities/by-query` + +This supports the same features as the `GET` variant, but in a `POST` body to +not have to abide by URL length limits. Additionally, it supports advanced, more +expressive querying format - see below. The response format is identical. + +#### Querying by filter predicate + +You can pass in a filter predicate to select a subset of entities in the +catalog. They are comprised of an optional logical expression tree (using +`$all`, `$any`, `$not`), ending in filter sets that can have custom matchers +(e.g. `$exists`, `$in`, `$hasPrefix`, `$contains`). + +This is an example of what such a filter predicate expression might look like: + +```js +{ + "query": { + "$all": [ + { + "kind": "Component", + "spec.type": { "$in": ["service", "website"] } + }, + { + "$not": { + "metadata.annotations.backstage.io/orphan": "true" + } + } + ] + } +} +``` + +A filter set is an object whose keys are dot separated paths into an object, and +the values are either primitives (string, number, or boolean) or custom matchers +as per below. An example of a simple such filter set is: + +```js +// All of the following must be true for a given entity (there's an +// implicit AND between them) +{ + // The kind field is matched against a literal, case insensitively + "kind": "Component", + // The type field inside the spec is matched using a custom matcher, see below + "spec.type": { "$in": ["service", "website"] } +} +``` + +The root of the query is always an object, whether there is a logic expression +tree or not. Nodes with a single key that starts with a `$` sign have special +meaning. + +- `$not`: Logical negation. + + Its value must be a single expression. Example: + + ```js + // Matches entities that do NOT have kind Component + { + "$not": { + "kind": "Component", + } + } + ``` + + Note that `$not` cannot be used in a right hand side value matcher. + + ```js + // ❌ WRONG + { "kind": { "$not": "Component" } } + // ✅ CORRECT + { "$not": { "kind": "Component" } } + ``` + +- `$all`: Require that all given expressions match each entity. + + Its value must be an array of expressions. Example: + + ```js + // Matches entities that BOTH have kind Component and type website + { + "$all": [ + { "kind": "Component" }, + { "spec.type": "website" } + ] + } + ``` + + An empty array always matches every entity. + +- `$any`: Require that at least one of a set of expressions match a given entity. + + Its value must be an array of expressions. Example: + + ```js + // Matches entities that EITHER have kind Component or type website + { + "$any": [ + { "kind": "Component" }, + { "spec.type": "website" } + ] + } + ``` + + An empty array never matches anything. + +- `$exists`: Assert on the existence of fields. + + Its value is either `true`, meaning that the field must exist on the entity + (no matter what its value), or `false`, meaning that it must not exist. + Example: + + ```js + // Matches entities that DO NOT have that annotation, ignoring what the + // value might be + { + "metadata.annotations.backstage.io/orphan": { + "$exists": false + }, + } + ``` + +- `$in`: Assert that a field has any of a set of primitive values. + + Its value must be an array of string, number, and/or boolean values. Example: + + ```js + // Matches entities whose type is EITHER service or website + { + "spec.type": { + "$in": ["service", "website"] + } + } + ``` + + The matching is case insensitive. An empty array never matches anything. + +- `$hasPrefix`: Assert that a field is a string that starts with a certain prefix text. + + Its value is a string. Example: + + ```js + // Matches entities whose project slug annotation starts with "backstage/" + { + "metadata.annotations.github.com/project-slug": { + "$hasPrefix": "backstage/" + } + } + ``` + + The matching is case insensitive, and captures both exact matches and strings + that start with the given prefix. + +- `$contains`: Assert that an array contains an element that matches the given expression. + + There is only limited support for this matcher. One use case is for relations: + + ```js + { + // Specifically type and (optionally) targetRef supported, and only + // with equality or "$in" for the targetRef + "relations": { + "$contains": { + "type": "ownedBy", + "targetRef": { + "$in": ["user:default/foo", "group:default/bar"] + } + } + } + } + ``` + + The other use case is for arrays where you match with a primitive value, such + as labels. Example: + + ```js + { + // Works for any field, as long as the value is a primitive + // (either string, number, or boolean) + "metadata.labels": { + "$contains": "java" + } + } + ``` + + In every case, the matching is case insensitive. + ### `GET /entities` Lists entities. diff --git a/packages/catalog-client/report.api.md b/packages/catalog-client/report.api.md index cc83cce443..0d71e6bc59 100644 --- a/packages/catalog-client/report.api.md +++ b/packages/catalog-client/report.api.md @@ -8,7 +8,7 @@ import type { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; import type { FilterPredicate } from '@backstage/filter-predicates'; -import { SerializedError } from '@backstage/errors'; +import type { SerializedError } from '@backstage/errors'; // @public export type AddLocationRequest = { diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 64d01c0972..c39c0c872e 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; -import { SerializedError } from '@backstage/errors'; +import type { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import type { SerializedError } from '@backstage/errors'; import type { AnalyzeLocationRequest, AnalyzeLocationResponse, @@ -438,7 +438,9 @@ export type QueryEntitiesInitialRequest = { */ filter?: EntityFilterQuery; /** - * Predicate-based filter with logical operators ($all, $any, $not, $exists, $in, $hasPrefix). + * Predicate-based filter with operators for logical expressions (`$all`, + * `$any`, and `$not`) and matching (`$exists`, `$in`, `$hasPrefix`, and + * (partially) `$contains`). * * @example * ```typescript @@ -446,7 +448,7 @@ export type QueryEntitiesInitialRequest = { * query: { * $all: [ * { kind: 'component' }, - * { 'spec.type': 'service' } + * { 'spec.type': { $in: ['service', 'website'] } } * ] * } * }