From f0876da49f6a98baa378b28a9e67218f15528d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 20 Feb 2026 10:35:15 +0100 Subject: [PATCH] use a structured orderBy for ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../catalog-client/src/CatalogClient.test.ts | 12 ++++--- packages/catalog-client/src/CatalogClient.ts | 4 +-- .../QueryEntitiesByPredicateRequest.model.ts | 3 +- ...iesByPredicateRequestOrderByInner.model.ts | 34 +++++++++++++++++++ .../schema/openapi/generated/models/index.ts | 1 + .../catalog-backend/src/schema/openapi.yaml | 15 ++++++-- .../QueryEntitiesByPredicateRequest.model.ts | 3 +- ...iesByPredicateRequestOrderByInner.model.ts | 34 +++++++++++++++++++ .../schema/openapi/generated/models/index.ts | 1 + .../src/schema/openapi/generated/router.ts | 14 ++++++-- .../service/request/parseEntityQuery.test.ts | 14 +++++--- .../src/service/request/parseEntityQuery.ts | 11 +++--- 12 files changed, 122 insertions(+), 24 deletions(-) create mode 100644 packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts create mode 100644 plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 4ee93c0cba..aee10cd3f2 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -751,9 +751,11 @@ describe('CatalogClient', () => { expect(mockedEndpoint).toHaveBeenCalledTimes(1); }); - it('should send orderFields with correct format (field,order)', async () => { + it('should send orderFields with correct format', async () => { const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { - expect(req.body.orderField).toEqual(['metadata.name,asc']); + expect(req.body.orderBy).toEqual([ + { field: 'metadata.name', order: 'asc' }, + ]); return res(ctx.json(defaultResponse)); }); @@ -769,9 +771,9 @@ describe('CatalogClient', () => { it('should send multiple orderFields with correct format', async () => { const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { - expect(req.body.orderField).toEqual([ - 'metadata.name,asc', - 'spec.type,desc', + expect(req.body.orderBy).toEqual([ + { field: 'metadata.name', order: 'asc' }, + { field: 'spec.type', order: 'desc' }, ]); return res(ctx.json(defaultResponse)); }); diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 51d57f715e..a6f47933f2 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -368,9 +368,7 @@ export class CatalogClient implements CatalogApi { body.limit = limit; } if (orderFields !== undefined) { - body.orderField = ( - Array.isArray(orderFields) ? orderFields : [orderFields] - ).map(({ field, order }) => `${field},${order}`); + body.orderBy = [orderFields].flat(); } if (fullTextFilter) { body.fullTextFilter = fullTextFilter; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts index bdcd854a5c..818a780b4a 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts @@ -19,6 +19,7 @@ // ****************************************************************** import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +import { QueryEntitiesByPredicateRequestOrderByInner } from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; /** * @public @@ -26,7 +27,7 @@ import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEn export interface QueryEntitiesByPredicateRequest { cursor?: string; limit?: number; - orderField?: Array; + orderBy?: Array; fullTextFilter?: QueryEntitiesByPredicateRequestFullTextFilter; fields?: Array; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts new file mode 100644 index 0000000000..373ca9715f --- /dev/null +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts @@ -0,0 +1,34 @@ +/* + * 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 QueryEntitiesByPredicateRequestOrderByInner { + field: string; + order: QueryEntitiesByPredicateRequestOrderByInnerOrderEnum; +} + +/** + * @public + */ +export type QueryEntitiesByPredicateRequestOrderByInnerOrderEnum = + | 'asc' + | 'desc'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/index.ts b/packages/catalog-client/src/schema/openapi/generated/models/index.ts index 889b5968fa..9461ac8d28 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/index.ts @@ -47,6 +47,7 @@ export * from '../models/ModelError.model'; export * from '../models/NullableEntity.model'; export * from '../models/QueryEntitiesByPredicateRequest.model'; export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; export * from '../models/RecursivePartialEntity.model'; export * from '../models/RecursivePartialEntityMeta.model'; export * from '../models/RecursivePartialEntityMetaAllOf.model'; diff --git a/plugins/catalog-backend/src/schema/openapi.yaml b/plugins/catalog-backend/src/schema/openapi.yaml index f44ac026e4..41738c1148 100644 --- a/plugins/catalog-backend/src/schema/openapi.yaml +++ b/plugins/catalog-backend/src/schema/openapi.yaml @@ -1125,10 +1125,21 @@ paths: type: string limit: type: number - orderField: + orderBy: type: array items: - type: string + type: object + required: + - field + - order + properties: + field: + type: string + order: + type: string + enum: + - asc + - desc fullTextFilter: type: object properties: diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts index bdcd854a5c..818a780b4a 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts @@ -19,6 +19,7 @@ // ****************************************************************** import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +import { QueryEntitiesByPredicateRequestOrderByInner } from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; /** * @public @@ -26,7 +27,7 @@ import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEn export interface QueryEntitiesByPredicateRequest { cursor?: string; limit?: number; - orderField?: Array; + orderBy?: Array; fullTextFilter?: QueryEntitiesByPredicateRequestFullTextFilter; fields?: Array; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts new file mode 100644 index 0000000000..373ca9715f --- /dev/null +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts @@ -0,0 +1,34 @@ +/* + * 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 QueryEntitiesByPredicateRequestOrderByInner { + field: string; + order: QueryEntitiesByPredicateRequestOrderByInnerOrderEnum; +} + +/** + * @public + */ +export type QueryEntitiesByPredicateRequestOrderByInnerOrderEnum = + | 'asc' + | 'desc'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts index 889b5968fa..9461ac8d28 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts @@ -47,6 +47,7 @@ export * from '../models/ModelError.model'; export * from '../models/NullableEntity.model'; export * from '../models/QueryEntitiesByPredicateRequest.model'; export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; export * from '../models/RecursivePartialEntity.model'; export * from '../models/RecursivePartialEntityMeta.model'; export * from '../models/RecursivePartialEntityMetaAllOf.model'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/router.ts b/plugins/catalog-backend/src/schema/openapi/generated/router.ts index 86ddeddf61..ed9536d6b9 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/router.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/router.ts @@ -1269,10 +1269,20 @@ export const spec = { limit: { type: 'number', }, - orderField: { + orderBy: { type: 'array', items: { - type: 'string', + type: 'object', + required: ['field', 'order'], + properties: { + field: { + type: 'string', + }, + order: { + type: 'string', + enum: ['asc', 'desc'], + }, + }, }, }, fullTextFilter: { diff --git a/plugins/catalog-backend/src/service/request/parseEntityQuery.test.ts b/plugins/catalog-backend/src/service/request/parseEntityQuery.test.ts index 8b6d1c21b7..b95c4c6c0e 100644 --- a/plugins/catalog-backend/src/service/request/parseEntityQuery.test.ts +++ b/plugins/catalog-backend/src/service/request/parseEntityQuery.test.ts @@ -77,9 +77,12 @@ describe('parseEntityQuery', () => { ); }); - it('parses orderField into orderFields', () => { + it('parses orderBy into orderFields', () => { const result = parseEntityQuery({ - orderField: ['metadata.name,asc', 'metadata.namespace,desc'], + orderBy: [ + { field: 'metadata.name', order: 'asc' }, + { field: 'metadata.namespace', order: 'desc' }, + ], }); expect(result).toEqual( expect.objectContaining({ @@ -128,9 +131,12 @@ describe('parseEntityQuery', () => { ); }); - it('throws on invalid orderField order value', () => { + it('throws on invalid orderBy order value', () => { expect(() => - parseEntityQuery({ orderField: ['metadata.name,sideways'] }), + parseEntityQuery({ + // @ts-expect-error - invalid order value + orderBy: [{ field: 'metadata.name', order: 'sideways' }], + }), ).toThrow(/Invalid order field order/); }); }); diff --git a/plugins/catalog-backend/src/service/request/parseEntityQuery.ts b/plugins/catalog-backend/src/service/request/parseEntityQuery.ts index 5ad505b8a1..c33492e740 100644 --- a/plugins/catalog-backend/src/service/request/parseEntityQuery.ts +++ b/plugins/catalog-backend/src/service/request/parseEntityQuery.ts @@ -41,17 +41,16 @@ function isSupportedFilterPredicateRoot( } function parseOrderFields( - orderField: string[] | undefined, + orderField: Array<{ field: string; order: string }> | undefined, ): EntityOrder[] | undefined { if (!orderField?.length) { return undefined; } - return orderField.map(entry => { - const [field, order] = entry.split(','); - if (order !== undefined && order !== 'asc' && order !== 'desc') { + return orderField.map(({ field, order }) => { + if (order !== 'asc' && order !== 'desc') { throw new InputError('Invalid order field order, must be asc or desc'); } - return { field, order: order as 'asc' | 'desc' }; + return { field, order }; }); } @@ -98,7 +97,7 @@ export function parseEntityQuery( query = result.data; } - const orderFields = parseOrderFields(request.orderField); + const orderFields = parseOrderFields(request.orderBy); return { query,