catalog-backend: allow processors to set location keys for emitted entities

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-01-28 12:09:19 +01:00
parent 78aff31f0d
commit 9b9737c107
6 changed files with 89 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Added support for emitting entities with an explicit location key during processing.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-node': minor
---
Added the `locationKey` option to `processingResult.entity(...)`.
@@ -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,
@@ -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),
});
});
});
@@ -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,
};
},
/**
@@ -155,6 +155,7 @@ export type CatalogProcessorEntityResult = {
type: 'entity';
entity: Entity;
location: LocationSpec;
locationKey?: string | null;
};
/** @public */