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:
Nilay1999
2026-01-12 11:32:28 +05:30
committed by benjdlambert
parent e5ee4d8da7
commit b883042fac
19 changed files with 840 additions and 1 deletions
@@ -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 };
@@ -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>;
}
@@ -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>;
}
@@ -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;
}
@@ -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;
@@ -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;
}
@@ -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>;
}
@@ -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;
@@ -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;
}
@@ -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;
}
@@ -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',