feat(catalog-backend): ✨ add post api for predicate-based entity filtering
- Add new /entities/by-predicates POST endpoint for expressive entity queries - Introduce EntityPredicate schema supporting logical operators ($all, $any, $not) - Add EntityPredicateValue schema with field operators ($exists, $in) - Generate OpenAPI models for predicate-based filtering - Update failing test case files Signed-off-by: Nilay1999 <nilayparmar19@gmail.com>
This commit is contained in:
@@ -338,6 +338,71 @@ components:
|
||||
properties: {}
|
||||
description: A type representing all allowed JSON object values.
|
||||
additionalProperties: {}
|
||||
EntityPredicate:
|
||||
description: |
|
||||
A predicate-based filter supporting logical operators.
|
||||
- $all: All conditions must match (AND)
|
||||
- $any: At least one condition must match (OR)
|
||||
- $not: Negates the condition
|
||||
- $exists: Check if field exists
|
||||
- $in: Match any value in array
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
$all:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EntityPredicate'
|
||||
required:
|
||||
- $all
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
$any:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EntityPredicate'
|
||||
required:
|
||||
- $any
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
$not:
|
||||
$ref: '#/components/schemas/EntityPredicate'
|
||||
required:
|
||||
- $not
|
||||
- type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/EntityPredicateValue'
|
||||
EntityPredicateValue:
|
||||
description: Value for a field predicate
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
$exists:
|
||||
type: boolean
|
||||
required:
|
||||
- $exists
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
$in:
|
||||
type: array
|
||||
items:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
required:
|
||||
- $in
|
||||
MapStringString:
|
||||
type: object
|
||||
properties: {}
|
||||
@@ -1095,6 +1160,106 @@ paths:
|
||||
type: string
|
||||
explode: false
|
||||
style: form
|
||||
/entities/by-predicates:
|
||||
post:
|
||||
operationId: GetEntitiesByPredicates
|
||||
tags:
|
||||
- Entity
|
||||
description: |
|
||||
Query entities using predicate-based filters. This endpoint supports a more
|
||||
expressive filter syntax with logical operators ($all, $any, $not) and
|
||||
value operators ($exists, $in).
|
||||
|
||||
Example filter:
|
||||
```json
|
||||
{
|
||||
"filter": {
|
||||
"$all": [
|
||||
{"kind": "component"},
|
||||
{"$any": [
|
||||
{"spec.type": "service"},
|
||||
{"spec.type": "website"}
|
||||
]},
|
||||
{"$not": {"spec.lifecycle": "experimental"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Entity'
|
||||
description: The list of entities matching the predicate filter.
|
||||
pageInfo:
|
||||
type: object
|
||||
properties:
|
||||
nextCursor:
|
||||
type: string
|
||||
description: The cursor for the next batch of entities.
|
||||
required:
|
||||
- items
|
||||
- pageInfo
|
||||
'400':
|
||||
$ref: '#/components/responses/ErrorResponse'
|
||||
default:
|
||||
$ref: '#/components/responses/ErrorResponse'
|
||||
security:
|
||||
- {}
|
||||
- JWT: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/limit'
|
||||
- $ref: '#/components/parameters/offset'
|
||||
- $ref: '#/components/parameters/after'
|
||||
- name: order
|
||||
in: query
|
||||
allowReserved: true
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
filter:
|
||||
$ref: '#/components/schemas/EntityPredicate'
|
||||
examples:
|
||||
Get all service components:
|
||||
value:
|
||||
filter:
|
||||
$all:
|
||||
- kind: component
|
||||
- spec.type: service
|
||||
Get components owned by specific teams:
|
||||
value:
|
||||
filter:
|
||||
$all:
|
||||
- kind: component
|
||||
- spec.owner:
|
||||
$in:
|
||||
- backend-team
|
||||
- platform-team
|
||||
Get non-production services:
|
||||
value:
|
||||
filter:
|
||||
$all:
|
||||
- kind: component
|
||||
- spec.type: service
|
||||
- $not:
|
||||
spec.lifecycle: production
|
||||
/entity-facets:
|
||||
get:
|
||||
operationId: GetEntityFacets
|
||||
|
||||
@@ -25,6 +25,8 @@ import { EntitiesQueryResponse } from '../models/EntitiesQueryResponse.model';
|
||||
import { Entity } from '../models/Entity.model';
|
||||
import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model';
|
||||
import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model';
|
||||
import { GetEntitiesByPredicates200Response } from '../models/GetEntitiesByPredicates200Response.model';
|
||||
import { GetEntitiesByPredicatesRequest } from '../models/GetEntitiesByPredicatesRequest.model';
|
||||
import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model';
|
||||
import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model';
|
||||
import { ValidateEntity400Response } from '../models/ValidateEntity400Response.model';
|
||||
@@ -53,6 +55,19 @@ export type GetEntities = {
|
||||
};
|
||||
response: Array<Entity> | Error | Error;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type GetEntitiesByPredicates = {
|
||||
body: GetEntitiesByPredicatesRequest;
|
||||
query: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
after?: string;
|
||||
order?: Array<string>;
|
||||
};
|
||||
response: GetEntitiesByPredicates200Response | Error | Error;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -208,6 +223,8 @@ export type EndpointMap = {
|
||||
|
||||
'#get|/entities': GetEntities;
|
||||
|
||||
'#post|/entities/by-predicates': GetEntitiesByPredicates;
|
||||
|
||||
'#get|/entities/by-query': GetEntitiesByQuery;
|
||||
|
||||
'#post|/entities/by-refs': GetEntitiesByRefs;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { EntityPredicateOneOf } from '../models/EntityPredicateOneOf.model';
|
||||
import { EntityPredicateOneOf1 } from '../models/EntityPredicateOneOf1.model';
|
||||
import { EntityPredicateOneOf2 } from '../models/EntityPredicateOneOf2.model';
|
||||
import { EntityPredicateValue } from '../models/EntityPredicateValue.model';
|
||||
|
||||
/**
|
||||
* A predicate-based filter supporting logical operators. - $all: All conditions must match (AND) - $any: At least one condition must match (OR) - $not: Negates the condition - $exists: Check if field exists - $in: Match any value in array
|
||||
* @public
|
||||
*/
|
||||
export type EntityPredicate =
|
||||
| EntityPredicateOneOf
|
||||
| EntityPredicateOneOf1
|
||||
| EntityPredicateOneOf2
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| { [key: string]: EntityPredicateValue };
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { EntityPredicate } from '../models/EntityPredicate.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface EntityPredicateOneOf {
|
||||
$all: Array<EntityPredicate>;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { EntityPredicate } from '../models/EntityPredicate.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface EntityPredicateOneOf1 {
|
||||
$any: Array<EntityPredicate>;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { EntityPredicate } from '../models/EntityPredicate.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface EntityPredicateOneOf2 {
|
||||
$not: EntityPredicate;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { EntityPredicateValueOneOf } from '../models/EntityPredicateValueOneOf.model';
|
||||
import { EntityPredicateValueOneOf1 } from '../models/EntityPredicateValueOneOf1.model';
|
||||
|
||||
/**
|
||||
* Value for a field predicate
|
||||
* @public
|
||||
*/
|
||||
export type EntityPredicateValue =
|
||||
| EntityPredicateValueOneOf
|
||||
| EntityPredicateValueOneOf1
|
||||
| boolean
|
||||
| number
|
||||
| string;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface EntityPredicateValueOneOf {
|
||||
$exists: boolean;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { EntityPredicateValueOneOf1InInner } from '../models/EntityPredicateValueOneOf1InInner.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface EntityPredicateValueOneOf1 {
|
||||
$in: Array<EntityPredicateValueOneOf1InInner>;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type EntityPredicateValueOneOf1InInner = boolean | number | string;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { Entity } from '../models/Entity.model';
|
||||
import { GetEntitiesByPredicates200ResponsePageInfo } from '../models/GetEntitiesByPredicates200ResponsePageInfo.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface GetEntitiesByPredicates200Response {
|
||||
/**
|
||||
* The list of entities matching the predicate filter.
|
||||
*/
|
||||
items: Array<Entity>;
|
||||
pageInfo: GetEntitiesByPredicates200ResponsePageInfo;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface GetEntitiesByPredicates200ResponsePageInfo {
|
||||
/**
|
||||
* The cursor for the next batch of entities.
|
||||
*/
|
||||
nextCursor?: string;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { EntityPredicate } from '../models/EntityPredicate.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface GetEntitiesByPredicatesRequest {
|
||||
filter?: EntityPredicate;
|
||||
}
|
||||
@@ -31,10 +31,21 @@ export * from '../models/EntityFacet.model';
|
||||
export * from '../models/EntityFacetsResponse.model';
|
||||
export * from '../models/EntityLink.model';
|
||||
export * from '../models/EntityMeta.model';
|
||||
export * from '../models/EntityPredicate.model';
|
||||
export * from '../models/EntityPredicateOneOf.model';
|
||||
export * from '../models/EntityPredicateOneOf1.model';
|
||||
export * from '../models/EntityPredicateOneOf2.model';
|
||||
export * from '../models/EntityPredicateValue.model';
|
||||
export * from '../models/EntityPredicateValueOneOf.model';
|
||||
export * from '../models/EntityPredicateValueOneOf1.model';
|
||||
export * from '../models/EntityPredicateValueOneOf1InInner.model';
|
||||
export * from '../models/EntityRelation.model';
|
||||
export * from '../models/ErrorError.model';
|
||||
export * from '../models/ErrorRequest.model';
|
||||
export * from '../models/ErrorResponse.model';
|
||||
export * from '../models/GetEntitiesByPredicates200Response.model';
|
||||
export * from '../models/GetEntitiesByPredicates200ResponsePageInfo.model';
|
||||
export * from '../models/GetEntitiesByPredicatesRequest.model';
|
||||
export * from '../models/GetEntitiesByRefsRequest.model';
|
||||
export * from '../models/GetLocations200ResponseInner.model';
|
||||
export * from '../models/GetLocationsByQueryRequest.model';
|
||||
|
||||
@@ -262,6 +262,110 @@ export const spec = {
|
||||
description: 'A type representing all allowed JSON object values.',
|
||||
additionalProperties: {},
|
||||
},
|
||||
EntityPredicate: {
|
||||
description:
|
||||
'A predicate-based filter supporting logical operators.\n- $all: All conditions must match (AND)\n- $any: At least one condition must match (OR)\n- $not: Negates the condition\n- $exists: Check if field exists\n- $in: Match any value in array\n',
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
$all: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/EntityPredicate',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['$all'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
$any: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/EntityPredicate',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['$any'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
$not: {
|
||||
$ref: '#/components/schemas/EntityPredicate',
|
||||
},
|
||||
},
|
||||
required: ['$not'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
$ref: '#/components/schemas/EntityPredicateValue',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
EntityPredicateValue: {
|
||||
description: 'Value for a field predicate',
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
$exists: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
required: ['$exists'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
$in: {
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['$in'],
|
||||
},
|
||||
],
|
||||
},
|
||||
MapStringString: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -1229,6 +1333,149 @@ export const spec = {
|
||||
],
|
||||
},
|
||||
},
|
||||
'/entities/by-predicates': {
|
||||
post: {
|
||||
operationId: 'GetEntitiesByPredicates',
|
||||
tags: ['Entity'],
|
||||
description:
|
||||
'Query entities using predicate-based filters. This endpoint supports a more\nexpressive filter syntax with logical operators ($all, $any, $not) and\nvalue operators ($exists, $in).\n\nExample filter:\n```json\n{\n "filter": {\n "$all": [\n {"kind": "component"},\n {"$any": [\n {"spec.type": "service"},\n {"spec.type": "website"}\n ]},\n {"$not": {"spec.lifecycle": "experimental"}}\n ]\n }\n}\n```\n',
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ok',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/Entity',
|
||||
},
|
||||
description:
|
||||
'The list of entities matching the predicate filter.',
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The cursor for the next batch of entities.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['items', 'pageInfo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'400': {
|
||||
$ref: '#/components/responses/ErrorResponse',
|
||||
},
|
||||
default: {
|
||||
$ref: '#/components/responses/ErrorResponse',
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{},
|
||||
{
|
||||
JWT: [],
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
$ref: '#/components/parameters/limit',
|
||||
},
|
||||
{
|
||||
$ref: '#/components/parameters/offset',
|
||||
},
|
||||
{
|
||||
$ref: '#/components/parameters/after',
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
in: 'query',
|
||||
allowReserved: true,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
filter: {
|
||||
$ref: '#/components/schemas/EntityPredicate',
|
||||
},
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
'Get all service components': {
|
||||
value: {
|
||||
filter: {
|
||||
$all: [
|
||||
{
|
||||
kind: 'component',
|
||||
},
|
||||
{
|
||||
'spec.type': 'service',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'Get components owned by specific teams': {
|
||||
value: {
|
||||
filter: {
|
||||
$all: [
|
||||
{
|
||||
kind: 'component',
|
||||
},
|
||||
{
|
||||
'spec.owner': {
|
||||
$in: ['backend-team', 'platform-team'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'Get non-production services': {
|
||||
value: {
|
||||
filter: {
|
||||
$all: [
|
||||
{
|
||||
kind: 'component',
|
||||
},
|
||||
{
|
||||
'spec.type': 'service',
|
||||
},
|
||||
{
|
||||
$not: {
|
||||
'spec.lifecycle': 'production',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/entity-facets': {
|
||||
get: {
|
||||
operationId: 'GetEntityFacets',
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('AuthorizedEntitiesCatalog', () => {
|
||||
const fakeCatalog = {
|
||||
entities: jest.fn(),
|
||||
entitiesBatch: jest.fn(),
|
||||
queryEntitiesByPredicate: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
entityAncestry: jest.fn(),
|
||||
facets: jest.fn(),
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
EntityAncestryResponse,
|
||||
EntityFacetsRequest,
|
||||
EntityFacetsResponse,
|
||||
EntityPredicateRequest,
|
||||
QueryEntitiesRequest,
|
||||
QueryEntitiesResponse,
|
||||
} from '../catalog/types';
|
||||
@@ -196,6 +197,36 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog {
|
||||
return this.entitiesCatalog.queryEntities(request);
|
||||
}
|
||||
|
||||
async queryEntitiesByPredicate(
|
||||
request?: EntityPredicateRequest,
|
||||
): Promise<EntitiesResponse> {
|
||||
if (!request) {
|
||||
return {
|
||||
entities: { type: 'object', entities: [] },
|
||||
pageInfo: { hasNextPage: false },
|
||||
};
|
||||
}
|
||||
|
||||
const authorizeDecision = (
|
||||
await this.permissionApi.authorizeConditional(
|
||||
[{ permission: catalogEntityReadPermission }],
|
||||
{ credentials: request.credentials },
|
||||
)
|
||||
)[0];
|
||||
|
||||
if (authorizeDecision.result === AuthorizeResult.DENY) {
|
||||
return {
|
||||
entities: { type: 'object', entities: [] },
|
||||
pageInfo: { hasNextPage: false },
|
||||
};
|
||||
}
|
||||
|
||||
// Note: For CONDITIONAL results, we pass through to the underlying catalog
|
||||
// since EntityPredicate filters are separate from EntityFilter permission conditions.
|
||||
// The permission filter would need to be applied separately if needed.
|
||||
return this.entitiesCatalog.queryEntitiesByPredicate(request);
|
||||
}
|
||||
|
||||
async removeEntityByUid(
|
||||
uid: string,
|
||||
options: { credentials: BackstageCredentials },
|
||||
|
||||
@@ -72,6 +72,7 @@ describe('createRouter readonly disabled', () => {
|
||||
beforeEach(async () => {
|
||||
entitiesCatalog = {
|
||||
entities: jest.fn(),
|
||||
queryEntitiesByPredicate: jest.fn(),
|
||||
entitiesBatch: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
entityAncestry: jest.fn(),
|
||||
@@ -1120,6 +1121,7 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
beforeAll(async () => {
|
||||
entitiesCatalog = {
|
||||
entities: jest.fn(),
|
||||
queryEntitiesByPredicate: jest.fn(),
|
||||
entitiesBatch: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
entityAncestry: jest.fn(),
|
||||
@@ -1336,6 +1338,7 @@ describe('NextRouter permissioning', () => {
|
||||
beforeAll(async () => {
|
||||
entitiesCatalog = {
|
||||
entities: jest.fn(),
|
||||
queryEntitiesByPredicate: jest.fn(),
|
||||
entitiesBatch: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
entityAncestry: jest.fn(),
|
||||
|
||||
@@ -30,7 +30,10 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError, serializeError } from '@backstage/errors';
|
||||
import { LocationAnalyzer } from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
EntityPredicate,
|
||||
LocationAnalyzer,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import express from 'express';
|
||||
import yn from 'yn';
|
||||
import { z } from 'zod';
|
||||
@@ -258,6 +261,53 @@ export async function createRouter(
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
.post('/entities/by-predicates', async (req, res) => {
|
||||
const auditorEvent = await auditor.createEvent({
|
||||
eventId: 'entity-fetch',
|
||||
request: req,
|
||||
meta: {
|
||||
queryType: 'by-predicates',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const filter = req.body.filter as EntityPredicate | undefined;
|
||||
const order = parseEntityOrderParams(req.query);
|
||||
const pagination = parseEntityPaginationParams(req.query);
|
||||
const credentials = await httpAuth.credentials(req);
|
||||
|
||||
const { entities, pageInfo } =
|
||||
await entitiesCatalog.queryEntitiesByPredicate({
|
||||
filter,
|
||||
order,
|
||||
pagination,
|
||||
credentials,
|
||||
});
|
||||
|
||||
const meta = {
|
||||
pageInfo: {
|
||||
...(pageInfo.hasNextPage && {
|
||||
nextCursor: pageInfo.endCursor,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
await auditorEvent?.success({ meta });
|
||||
|
||||
await writeEntitiesResponse({
|
||||
res,
|
||||
items: entities,
|
||||
alwaysUseObjectMode: enableRelationsCompatibility,
|
||||
responseWrapper: items => ({
|
||||
items,
|
||||
...meta,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
await auditorEvent?.fail({ error: err });
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
.get('/entities/by-query', async (req, res) => {
|
||||
const auditorEvent = await auditor.createEvent({
|
||||
eventId: 'entity-fetch',
|
||||
|
||||
Reference in New Issue
Block a user