Merge pull request #16669 from drodil/scaffolder_get_entities
feat: add scaffolder action to get multiple entities
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Extended scaffolder action `catalog:fetch` to fetch multiple catalog entities by entity references.
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user