From d675d96cc1c7120b3800f8a3b9bac93ed42a70e2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 15 Jul 2025 15:44:03 +0200 Subject: [PATCH] catalog-backend: disable relations compatibility by default Signed-off-by: Patrik Oldsberg --- .changeset/cute-sloths-smile.md | 5 + plugins/catalog-backend/config.d.ts | 8 +- .../src/service/CatalogBuilder.ts | 8 +- .../service/DefaultEntitiesCatalog.test.ts | 3 +- .../src/service/DefaultEntitiesCatalog.ts | 16 +-- .../src/service/createRouter.test.ts | 124 +++++++++--------- .../src/service/createRouter.ts | 12 +- .../src/tests/integration.test.ts | 5 +- 8 files changed, 92 insertions(+), 89 deletions(-) create mode 100644 .changeset/cute-sloths-smile.md diff --git a/.changeset/cute-sloths-smile.md b/.changeset/cute-sloths-smile.md new file mode 100644 index 0000000000..cc71d2e7ab --- /dev/null +++ b/.changeset/cute-sloths-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': major +--- + +**BREAKING**: The relations compatibility mode is no longer enabled by default, and the `disableRelationsCompatiblity` flag has been removed. To re-enable relations compatibility, the new `enableRelationsCompatibility` flag can be used instead. diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 3ba6047ada..df7d93006b 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -139,14 +139,14 @@ export interface Config { }>; /** - * Disables the compatibility layer for relations in returned entities that + * Enables the compatibility layer for relations in returned entities that * ensures that all relations objects have both `target` and `targetRef`. * - * Enabling this option significantly reduces the memory usage of the - * catalog, and slightly increases performance, but may break consumers that + * Enabling this option significantly increases the memory usage of the + * catalog, and slightly reduces performance, but may avoid breaking consumers that * rely on the existence of `target` in the relations objects. */ - disableRelationsCompatibility?: boolean; + enableRelationsCompatibility?: boolean; /** * Disables the default backstage processors. diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e50aa84d80..0521f3b497 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -463,8 +463,8 @@ export class CatalogBuilder { httpAuth, } = this.env; - const disableRelationsCompatibility = config.getOptionalBoolean( - 'catalog.disableRelationsCompatibility', + const enableRelationsCompatibility = Boolean( + config.getOptionalBoolean('catalog.enableRelationsCompatibility'), ); const policy = this.buildEntityPolicy(); @@ -503,7 +503,7 @@ export class CatalogBuilder { database: dbClient, logger, stitcher, - disableRelationsCompatibility, + enableRelationsCompatibility, }); let permissionsService: PermissionsService; @@ -619,7 +619,7 @@ export class CatalogBuilder { httpAuth, permissionsService, auditor, - disableRelationsCompatibility, + enableRelationsCompatibility, }); if ( diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index aad09d7de1..0b8dec71bb 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -525,7 +525,7 @@ describe('DefaultEntitiesCatalog', () => { ); it.each(databases.eachSupportedId())( - 'should return both target and targetRef for entities', + 'should return both target and targetRef for entities in compat mode', async databaseId => { await createDatabase(databaseId); await addEntity( @@ -557,6 +557,7 @@ describe('DefaultEntitiesCatalog', () => { database: knex, logger: mockServices.logger.mock(), stitcher, + enableRelationsCompatibility: true, }); const res = await catalog.entities(); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index a7b8fdc6c9..33847ea87b 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -105,19 +105,19 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { private readonly database: Knex; private readonly logger: LoggerService; private readonly stitcher: Stitcher; - private readonly disableRelationsCompatibility: boolean; + private readonly enableRelationsCompatibility: boolean; constructor(options: { database: Knex; logger: LoggerService; stitcher: Stitcher; - disableRelationsCompatibility?: boolean; + enableRelationsCompatibility?: boolean; }) { this.database = options.database; this.logger = options.logger; this.stitcher = options.stitcher; - this.disableRelationsCompatibility = Boolean( - options.disableRelationsCompatibility, + this.enableRelationsCompatibility = Boolean( + options.enableRelationsCompatibility, ); } @@ -199,15 +199,15 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { return { entities: processRawEntitiesResult( rows.map(r => r.final_entity!), - this.disableRelationsCompatibility - ? request?.fields - : e => { + this.enableRelationsCompatibility + ? e => { expandLegacyCompoundRelationsInEntity(e); if (request?.fields) { return request.fields(e); } return e; - }, + } + : request?.fields, ), pageInfo, }; diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 193d5b5b52..0246a3d56b 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -136,38 +136,6 @@ describe('createRouter readonly disabled', () => { { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, ]; - entitiesCatalog.entities.mockResolvedValueOnce({ - entities: { type: 'object', entities: [entities[0]] }, - pageInfo: { hasNextPage: false }, - }); - - const response = await request(app).get('/entities'); - - expect(response.status).toEqual(200); - expect(response.body).toEqual(entities); - }); - - it('happy path: lists entities when by-entities emulation is enabled', async () => { - const router = await createRouter({ - entitiesCatalog, - locationService, - orchestrator, - logger: mockServices.logger.mock(), - refreshService, - config: new ConfigReader(undefined), - permissionIntegrationRouter: express.Router(), - auth: mockServices.auth(), - httpAuth: mockServices.httpAuth(), - locationAnalyzer, - permissionsService, - disableRelationsCompatibility: true, // added - }); - app = await wrapServer(express().use(router)); - - const entities: Entity[] = [ - { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, - ]; - entitiesCatalog.queryEntities.mockResolvedValueOnce({ items: { type: 'object', entities: [entities[0]] }, pageInfo: {}, @@ -180,34 +148,7 @@ describe('createRouter readonly disabled', () => { expect(response.body).toEqual(entities); }); - it('parses single and multiple request parameters and passes them down', async () => { - entitiesCatalog.entities.mockResolvedValueOnce({ - entities: { type: 'object', entities: [] }, - pageInfo: { hasNextPage: false }, - }); - const response = await request(app).get( - '/entities?filter=a=1,a=2,b=3&filter=c=4', - ); - - expect(response.status).toEqual(200); - expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith({ - filter: { - anyOf: [ - { - allOf: [ - { key: 'a', values: ['1', '2'] }, - { key: 'b', values: ['3'] }, - ], - }, - { key: 'c', values: ['4'] }, - ], - }, - credentials: mockCredentials.user(), - }); - }); - - it('parses single and multiple request parameters and passes them down when by-entities emulation is enabled', async () => { + it('happy path: lists entities with relations compat', async () => { const router = await createRouter({ entitiesCatalog, locationService, @@ -220,10 +161,26 @@ describe('createRouter readonly disabled', () => { httpAuth: mockServices.httpAuth(), locationAnalyzer, permissionsService, - disableRelationsCompatibility: true, // added + enableRelationsCompatibility: true, // added }); - app = await wrapServer(express().use(router)); + app = await wrapServer(express().use(router)); + const entities: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + + entitiesCatalog.entities.mockResolvedValueOnce({ + entities: { type: 'object', entities: [entities[0]] }, + pageInfo: { hasNextPage: false }, + }); + + const response = await request(app).get('/entities'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(entities); + }); + + it('parses single and multiple request parameters and passes them down', async () => { entitiesCatalog.queryEntities.mockResolvedValueOnce({ items: { type: 'object', entities: [] }, pageInfo: {}, @@ -252,6 +209,48 @@ describe('createRouter readonly disabled', () => { skipTotalItems: true, }); }); + + it('parses single and multiple request parameters and passes them down with relations compat', async () => { + const router = await createRouter({ + entitiesCatalog, + locationService, + orchestrator, + logger: mockServices.logger.mock(), + refreshService, + config: new ConfigReader(undefined), + permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), + locationAnalyzer, + permissionsService, + enableRelationsCompatibility: true, + }); + app = await wrapServer(express().use(router)); + entitiesCatalog.entities.mockResolvedValueOnce({ + entities: { type: 'object', entities: [] }, + pageInfo: { hasNextPage: false }, + }); + const response = await request(app).get( + '/entities?filter=a=1,a=2,b=3&filter=c=4', + ); + + expect(response.status).toEqual(200); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filter: { + anyOf: [ + { + allOf: [ + { key: 'a', values: ['1', '2'] }, + { key: 'b', values: ['3'] }, + ], + }, + { key: 'c', values: ['4'] }, + ], + }, + credentials: mockCredentials.user(), + }); + }); }); describe('GET /entities/by-query', () => { @@ -941,7 +940,6 @@ describe('createRouter readonly and raw json enabled', () => { getLocationByEntity: jest.fn(), }; const router = await createRouter({ - disableRelationsCompatibility: true, entitiesCatalog, locationService, logger: mockServices.logger.mock(), diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index f4ce26b66b..2c9e63e8c4 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -81,7 +81,7 @@ export interface RouterOptions { permissionsService: PermissionsService; // TODO: Require AuditorService once `backend-legacy` is removed auditor?: AuditorService; - disableRelationsCompatibility?: boolean; + enableRelationsCompatibility?: boolean; } /** @@ -110,7 +110,7 @@ export async function createRouter( auth, httpAuth, auditor, - disableRelationsCompatibility = false, + enableRelationsCompatibility = false, } = options; const readonlyEnabled = @@ -179,7 +179,7 @@ export async function createRouter( // When pagination parameters are passed in, use the legacy slow path // that loads all entities into memory - if (pagination || disableRelationsCompatibility !== true) { + if (pagination || enableRelationsCompatibility === true) { const { entities, pageInfo } = await entitiesCatalog.entities({ filter, fields, @@ -204,7 +204,7 @@ export async function createRouter( await writeEntitiesResponse({ res, items: entities, - alwaysUseObjectMode: !disableRelationsCompatibility, + alwaysUseObjectMode: enableRelationsCompatibility, }); return; } @@ -295,7 +295,7 @@ export async function createRouter( await writeEntitiesResponse({ res, items, - alwaysUseObjectMode: !disableRelationsCompatibility, + alwaysUseObjectMode: enableRelationsCompatibility, responseWrapper: entities => ({ items: entities, ...meta, @@ -482,7 +482,7 @@ export async function createRouter( await writeEntitiesResponse({ res, items, - alwaysUseObjectMode: !disableRelationsCompatibility, + alwaysUseObjectMode: enableRelationsCompatibility, responseWrapper: entities => ({ items: entities, }), diff --git a/plugins/catalog-backend/src/tests/integration.test.ts b/plugins/catalog-backend/src/tests/integration.test.ts index 400bbc7538..719c5e6ccf 100644 --- a/plugins/catalog-backend/src/tests/integration.test.ts +++ b/plugins/catalog-backend/src/tests/integration.test.ts @@ -205,7 +205,7 @@ class TestHarness { readonly #db: Knex; static async create(options: { - disableRelationsCompatibility?: boolean; + enableRelationsCompatibility?: boolean; logger?: LoggerService; db: Knex; permissions?: PermissionEvaluator; @@ -283,7 +283,7 @@ class TestHarness { database: options.db, logger, stitcher, - disableRelationsCompatibility: options?.disableRelationsCompatibility, + enableRelationsCompatibility: options?.enableRelationsCompatibility, }); const proxyProgressTracker = new ProxyProgressTracker( new NoopProgressTracker(), @@ -863,7 +863,6 @@ describe('Catalog Backend Integration', () => { it('should return valid responses in raw JSON mode', async () => { const harness = await TestHarness.create({ db: await databases.init('SQLITE_3'), - disableRelationsCompatibility: true, }); const entityA = {