feat(catalog): add POST /entity-facets endpoint with predicate support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model';
|
||||
import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model';
|
||||
import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model';
|
||||
import { QueryEntitiesByPredicateRequest } from '../models/QueryEntitiesByPredicateRequest.model';
|
||||
import { QueryEntityFacetsByPredicateRequest } from '../models/QueryEntityFacetsByPredicateRequest.model';
|
||||
import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model';
|
||||
import { ValidateEntityRequest } from '../models/ValidateEntityRequest.model';
|
||||
import { AnalyzeLocationRequest } from '../models/AnalyzeLocationRequest.model';
|
||||
@@ -146,6 +147,12 @@ export type GetEntityFacets = {
|
||||
export type QueryEntitiesByPredicate = {
|
||||
body: QueryEntitiesByPredicateRequest;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type QueryEntityFacetsByPredicate = {
|
||||
body: QueryEntityFacetsByPredicateRequest;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -481,6 +488,31 @@ export class DefaultApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity facets using predicate-based filters.
|
||||
* @param queryEntityFacetsByPredicateRequest -
|
||||
*/
|
||||
public async queryEntityFacetsByPredicate(
|
||||
// @ts-ignore
|
||||
request: QueryEntityFacetsByPredicate,
|
||||
options?: RequestOptions,
|
||||
): Promise<TypedResponse<EntityFacetsResponse>> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
|
||||
|
||||
const uriTemplate = `/entity-facets`;
|
||||
|
||||
const uri = parser.parse(uriTemplate).expand({});
|
||||
|
||||
return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request.body),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the entity related to entityRef.
|
||||
* @param refreshEntityRequest -
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 QueryEntityFacetsByPredicateRequest {
|
||||
facets: Array<string>;
|
||||
/**
|
||||
* A type representing all allowed JSON object values.
|
||||
*/
|
||||
query?: { [key: string]: any };
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export * from '../models/NullableEntity.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequest.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model';
|
||||
export * from '../models/QueryEntityFacetsByPredicateRequest.model';
|
||||
export * from '../models/RecursivePartialEntity.model';
|
||||
export * from '../models/RecursivePartialEntityMeta.model';
|
||||
export * from '../models/RecursivePartialEntityMetaAllOf.model';
|
||||
|
||||
@@ -1194,6 +1194,40 @@ paths:
|
||||
value:
|
||||
- spec.type
|
||||
- $ref: '#/components/parameters/filter'
|
||||
post:
|
||||
operationId: QueryEntityFacetsByPredicate
|
||||
tags:
|
||||
- Entity
|
||||
description: Get entity facets using predicate-based filters.
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityFacetsResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/ErrorResponse'
|
||||
default:
|
||||
$ref: '#/components/responses/ErrorResponse'
|
||||
security:
|
||||
- {}
|
||||
- JWT: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- facets
|
||||
properties:
|
||||
facets:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
query:
|
||||
$ref: '#/components/schemas/JsonObject'
|
||||
/locations:
|
||||
post:
|
||||
operationId: CreateLocation
|
||||
|
||||
@@ -27,6 +27,7 @@ import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model';
|
||||
import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model';
|
||||
import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model';
|
||||
import { QueryEntitiesByPredicateRequest } from '../models/QueryEntitiesByPredicateRequest.model';
|
||||
import { QueryEntityFacetsByPredicateRequest } from '../models/QueryEntityFacetsByPredicateRequest.model';
|
||||
import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model';
|
||||
import { ValidateEntity400Response } from '../models/ValidateEntity400Response.model';
|
||||
import { ValidateEntityRequest } from '../models/ValidateEntityRequest.model';
|
||||
@@ -128,6 +129,13 @@ export type QueryEntitiesByPredicate = {
|
||||
body: QueryEntitiesByPredicateRequest;
|
||||
response: EntitiesQueryResponse | Error | Error;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type QueryEntityFacetsByPredicate = {
|
||||
body: QueryEntityFacetsByPredicateRequest;
|
||||
response: EntityFacetsResponse | Error | Error;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -230,6 +238,8 @@ export type EndpointMap = {
|
||||
|
||||
'#post|/entities/by-query': QueryEntitiesByPredicate;
|
||||
|
||||
'#post|/entity-facets': QueryEntityFacetsByPredicate;
|
||||
|
||||
'#post|/refresh': RefreshEntity;
|
||||
|
||||
'#post|/validate-entity': ValidateEntity;
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 QueryEntityFacetsByPredicateRequest {
|
||||
facets: Array<string>;
|
||||
/**
|
||||
* A type representing all allowed JSON object values.
|
||||
*/
|
||||
query?: { [key: string]: any };
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export * from '../models/NullableEntity.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequest.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model';
|
||||
export * from '../models/QueryEntityFacetsByPredicateRequest.model';
|
||||
export * from '../models/RecursivePartialEntity.model';
|
||||
export * from '../models/RecursivePartialEntityMeta.model';
|
||||
export * from '../models/RecursivePartialEntityMetaAllOf.model';
|
||||
|
||||
@@ -1373,6 +1373,57 @@ export const spec = {
|
||||
},
|
||||
],
|
||||
},
|
||||
post: {
|
||||
operationId: 'QueryEntityFacetsByPredicate',
|
||||
tags: ['Entity'],
|
||||
description: 'Get entity facets using predicate-based filters.',
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ok',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/EntityFacetsResponse',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'400': {
|
||||
$ref: '#/components/responses/ErrorResponse',
|
||||
},
|
||||
default: {
|
||||
$ref: '#/components/responses/ErrorResponse',
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{},
|
||||
{
|
||||
JWT: [],
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['facets'],
|
||||
properties: {
|
||||
facets: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
$ref: '#/components/schemas/JsonObject',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/locations': {
|
||||
post: {
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
parseQueryEntitiesParams,
|
||||
} from './request';
|
||||
import { parseEntityFacetParams } from './request/parseEntityFacetParams';
|
||||
import { parseEntityFacetsQuery } from './request/parseEntityFacetsQuery';
|
||||
import { parseEntityOrderParams } from './request/parseEntityOrderParams';
|
||||
import { parseEntityPaginationParams } from './request/parseEntityPaginationParams';
|
||||
import {
|
||||
@@ -564,6 +565,31 @@ export async function createRouter(
|
||||
|
||||
await auditorEvent?.success();
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (err) {
|
||||
await auditorEvent?.fail({
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
.post('/entity-facets', async (req, res) => {
|
||||
const auditorEvent = await auditor.createEvent({
|
||||
eventId: 'entity-facets',
|
||||
request: req,
|
||||
});
|
||||
|
||||
try {
|
||||
const { facets, query } = parseEntityFacetsQuery(req.body ?? {});
|
||||
|
||||
const response = await entitiesCatalog.facets({
|
||||
query,
|
||||
facets,
|
||||
credentials: await httpAuth.credentials(req),
|
||||
});
|
||||
|
||||
await auditorEvent?.success();
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (err) {
|
||||
await auditorEvent?.fail({
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
createZodV3FilterPredicateSchema,
|
||||
FilterPredicate,
|
||||
} from '@backstage/filter-predicates';
|
||||
import { z } from 'zod/v3';
|
||||
import { fromZodError } from 'zod-validation-error/v3';
|
||||
import { QueryEntityFacetsByPredicateRequest } from '../../schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model';
|
||||
|
||||
const filterPredicateSchema = createZodV3FilterPredicateSchema(z);
|
||||
|
||||
export interface ParsedEntityFacetsQuery {
|
||||
facets: string[];
|
||||
query?: FilterPredicate;
|
||||
}
|
||||
|
||||
export function parseEntityFacetsQuery(
|
||||
request: Readonly<QueryEntityFacetsByPredicateRequest>,
|
||||
): ParsedEntityFacetsQuery {
|
||||
// Parse facets
|
||||
if (!request.facets || request.facets.length === 0) {
|
||||
throw new InputError('Missing or empty facets parameter');
|
||||
}
|
||||
const facets = request.facets.filter(f => f.length > 0);
|
||||
if (facets.length === 0) {
|
||||
throw new InputError('Missing or empty facets parameter');
|
||||
}
|
||||
|
||||
// Parse query predicate
|
||||
let query: FilterPredicate | undefined;
|
||||
if (request.query !== undefined) {
|
||||
const result = filterPredicateSchema.safeParse(request.query);
|
||||
if (!result.success) {
|
||||
throw new InputError(`Invalid query: ${fromZodError(result.error)}`);
|
||||
}
|
||||
query = result.data;
|
||||
}
|
||||
|
||||
return { facets, query };
|
||||
}
|
||||
Reference in New Issue
Block a user