diff --git a/.changeset/fifty-humans-repair.md b/.changeset/fifty-humans-repair.md new file mode 100644 index 0000000000..28db460d63 --- /dev/null +++ b/.changeset/fifty-humans-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added support for emitting entities with an explicit location key during processing. diff --git a/.changeset/nine-ears-destroy.md b/.changeset/nine-ears-destroy.md new file mode 100644 index 0000000000..c987fd80ff --- /dev/null +++ b/.changeset/nine-ears-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': minor +--- + +Added the `locationKey` option to `processingResult.entity(...)`. diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index 0de6b12b90..ee488fc1d5 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -137,7 +137,9 @@ export class ProcessorOutputCollector { }, }; - this.deferredEntities.push({ entity, locationKey: location }); + const locationKey = + i.locationKey === undefined ? location : i.locationKey ?? undefined; + this.deferredEntities.push({ entity, locationKey }); } else if (i.type === 'location') { const entity = locationSpecToLocationEntity({ location: i.location, diff --git a/plugins/catalog-backend/src/tests/integration.test.ts b/plugins/catalog-backend/src/tests/integration.test.ts index 1c134d4c60..92d1b1c143 100644 --- a/plugins/catalog-backend/src/tests/integration.test.ts +++ b/plugins/catalog-backend/src/tests/integration.test.ts @@ -1166,4 +1166,61 @@ describe('Catalog Backend Integration', () => { 'component:default/b': withOutputFields(entityB), }); }); + + it('should be able to emit entities during processing with custom location keys', async () => { + const baseEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + }, + }, + }; + const entityA = merge({ metadata: { name: 'a' } }, baseEntity); + const entityB = merge({ metadata: { name: 'b' } }, baseEntity); + const entityBOverride = merge({ metadata: { override: true } }, entityB); + + const processEntity = jest.fn( + async ( + entity: Entity, + _location: LocationSpec, + _emit: CatalogProcessorEmit, + ) => entity, + ); + const harness = await TestHarness.create({ processEntity }); + + processEntity.mockImplementation(async (entity, location, emit) => { + if (entity.metadata.name === entityA.metadata.name) { + emit( + processingResult.entity(location, entityB, { + locationKey: null, + }), + ); + } + return entity; + }); + + // Start with just A, which outputs B via processing + await harness.setInputEntities([entityA]); + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/a': withOutputFields(entityA), + 'component:default/b': withOutputFields(entityB), + }); + + // Now apply A and B', which should override B since it has a null location key + await harness.setInputEntities([ + entityA, + { ...entityBOverride, locationKey: 'test' }, + ]); + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/a': withOutputFields(entityA), + 'component:default/b': withOutputFields(entityBOverride), + }); + }); }); diff --git a/plugins/catalog-node/src/api/processingResult.ts b/plugins/catalog-node/src/api/processingResult.ts index 0d4341fcb7..021160d347 100644 --- a/plugins/catalog-node/src/api/processingResult.ts +++ b/plugins/catalog-node/src/api/processingResult.ts @@ -76,8 +76,24 @@ export const processingResult = Object.freeze({ /** * Emits a child of the current entity, associated with a certain location. */ - entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult { - return { type: 'entity', location: atLocation, entity: newEntity }; + entity( + atLocation: LocationSpec, + newEntity: Entity, + options?: { + /** + * Sets the location key of the emitted entity, overriding the default one derived from the location. + * + * To set a `null` location key the value `null` must be used. + */ + locationKey?: string | null; + }, + ): CatalogProcessorResult { + return { + type: 'entity', + location: atLocation, + entity: newEntity, + locationKey: options?.locationKey, + }; }, /** diff --git a/plugins/catalog-node/src/api/processor.ts b/plugins/catalog-node/src/api/processor.ts index 3ad080f014..e43fdef071 100644 --- a/plugins/catalog-node/src/api/processor.ts +++ b/plugins/catalog-node/src/api/processor.ts @@ -155,6 +155,7 @@ export type CatalogProcessorEntityResult = { type: 'entity'; entity: Entity; location: LocationSpec; + locationKey?: string | null; }; /** @public */