Merge pull request #16669 from drodil/scaffolder_get_entities

feat: add scaffolder action to get multiple entities
This commit is contained in:
Ben Lambert
2023-03-14 13:50:47 +00:00
committed by GitHub
4 changed files with 188 additions and 37 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Extended scaffolder action `catalog:fetch` to fetch multiple catalog entities by entity references.
+2 -1
View File
@@ -89,7 +89,8 @@ export function createDebugLogAction(): TemplateAction_2<{
export function createFetchCatalogEntityAction(options: {
catalogClient: CatalogApi;
}): TemplateAction_2<{
entityRef: string;
entityRef?: string | undefined;
entityRefs?: string[] | undefined;
optional?: boolean | undefined;
}>;
@@ -23,8 +23,11 @@ import { createFetchCatalogEntityAction } from './fetch';
describe('catalog:fetch', () => {
const getEntityByRef = jest.fn();
const getEntitiesByRefs = jest.fn();
const catalogClient = {
getEntityByRef: getEntityByRef,
getEntitiesByRefs: getEntitiesByRefs,
};
const action = createFetchCatalogEntityAction({
@@ -71,25 +74,6 @@ describe('catalog:fetch', () => {
});
});
it('should return null if entity not in catalog and optional is true', async () => {
getEntityByRef.mockImplementationOnce(() => {
throw new Error('Not found');
});
await action.handler({
...mockContext,
input: {
entityRef: 'component:default/test',
optional: true,
},
});
expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', {
token: 'secret',
});
expect(mockContext.output).toHaveBeenCalledWith('entity', null);
});
it('should throw error if entity fetch fails from catalog and optional is false', async () => {
getEntityByRef.mockImplementationOnce(() => {
throw new Error('Not found');
@@ -127,4 +111,114 @@ describe('catalog:fetch', () => {
});
expect(mockContext.output).not.toHaveBeenCalled();
});
it('should return entities from catalog', async () => {
getEntitiesByRefs.mockReturnValueOnce({
items: [
{
metadata: {
namespace: 'default',
name: 'test',
},
kind: 'Component',
} as Entity,
],
});
await action.handler({
...mockContext,
input: {
entityRefs: ['component:default/test'],
},
});
expect(getEntitiesByRefs).toHaveBeenCalledWith(
{ entityRefs: ['component:default/test'] },
{
token: 'secret',
},
);
expect(mockContext.output).toHaveBeenCalledWith('entities', [
{
metadata: {
namespace: 'default',
name: 'test',
},
kind: 'Component',
},
]);
});
it('should throw error if undefined is returned for some entity', async () => {
getEntitiesByRefs.mockReturnValueOnce({
items: [
{
metadata: {
namespace: 'default',
name: 'test',
},
kind: 'Component',
} as Entity,
undefined,
],
});
await expect(
action.handler({
...mockContext,
input: {
entityRefs: ['component:default/test', 'component:default/test2'],
optional: false,
},
}),
).rejects.toThrow('Entity component:default/test2 not found');
expect(getEntitiesByRefs).toHaveBeenCalledWith(
{ entityRefs: ['component:default/test', 'component:default/test2'] },
{
token: 'secret',
},
);
expect(mockContext.output).not.toHaveBeenCalled();
});
it('should return null in case some of the entities not found and optional is true', async () => {
getEntitiesByRefs.mockReturnValueOnce({
items: [
{
metadata: {
namespace: 'default',
name: 'test',
},
kind: 'Component',
} as Entity,
undefined,
],
});
await action.handler({
...mockContext,
input: {
entityRefs: ['component:default/test', 'component:default/test2'],
optional: true,
},
});
expect(getEntitiesByRefs).toHaveBeenCalledWith(
{ entityRefs: ['component:default/test', 'component:default/test2'] },
{
token: 'secret',
},
);
expect(mockContext.output).toHaveBeenCalledWith('entities', [
{
metadata: {
namespace: 'default',
name: 'test',
},
kind: 'Component',
},
null,
]);
});
});
@@ -36,10 +36,26 @@ const examples = [
],
}),
},
{
description: 'Fetch multiple entities by referencse',
example: yaml.stringify({
steps: [
{
action: id,
id: 'fetchMultiple',
name: 'Fetch catalog entities',
input: {
entityRefs: ['component:default/name'],
},
},
],
}),
},
];
/**
* Returns entity from the catalog by entity reference.
* Returns entity or entities from the catalog by entity reference(s).
*
* @public
*/
export function createFetchCatalogEntityAction(options: {
@@ -47,13 +63,17 @@ export function createFetchCatalogEntityAction(options: {
}) {
const { catalogClient } = options;
return createTemplateAction<{ entityRef: string; optional?: boolean }>({
return createTemplateAction<{
entityRef?: string;
entityRefs?: string[];
optional?: boolean;
}>({
id,
description: 'Returns entity from the catalog by entity reference',
description:
'Returns entity or entities from the catalog by entity reference(s)',
examples,
schema: {
input: {
required: ['entityRef'],
type: 'object',
properties: {
entityRef: {
@@ -61,10 +81,15 @@ export function createFetchCatalogEntityAction(options: {
title: 'Entity reference',
description: 'Entity reference of the entity to get',
},
entityRefs: {
type: 'array',
title: 'Entity references',
description: 'Entity references of the entities to get',
},
optional: {
title: 'Optional',
description:
'Permit the entity to optionally exist. Default: false',
'Allow the entity or entities to optionally exist. Default: false',
type: 'boolean',
},
},
@@ -76,29 +101,55 @@ export function createFetchCatalogEntityAction(options: {
title: 'Entity found by the entity reference',
type: 'object',
description:
'Object containing same values used in the Entity schema.',
'Object containing same values used in the Entity schema. Only when used with `entityRef` parameter.',
},
entities: {
title: 'Entities found by the entity references',
type: 'array',
items: { type: 'object' },
description:
'Array containing objects with same values used in the Entity schema. Only when used with `entityRefs` parameter.',
},
},
},
},
async handler(ctx) {
const { entityRef, optional } = ctx.input;
let entity;
try {
entity = await catalogClient.getEntityByRef(entityRef, {
const { entityRef, entityRefs, optional } = ctx.input;
if (!entityRef && !entityRefs) {
if (optional) {
return;
}
throw new Error('Missing entity reference or references');
}
if (entityRef) {
const entity = await catalogClient.getEntityByRef(entityRef, {
token: ctx.secrets?.backstageToken,
});
} catch (e) {
if (!optional) {
throw e;
if (!entity && !optional) {
throw new Error(`Entity ${entityRef} not found`);
}
ctx.output('entity', entity ?? null);
}
if (!entity && !optional) {
throw new Error(`Entity ${entityRef} not found`);
}
if (entityRefs) {
const entities = await catalogClient.getEntitiesByRefs(
{ entityRefs },
{
token: ctx.secrets?.backstageToken,
},
);
ctx.output('entity', entity ?? null);
const finalEntities = entities.items.map((e, i) => {
if (!e && !optional) {
throw new Error(`Entity ${entityRefs[i]} not found`);
}
return e ?? null;
});
ctx.output('entities', finalEntities);
}
},
});
}