From f010b2cafceebe05a4ad4d8077c9ab3a724fd1de Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 19 Aug 2024 14:57:04 +0200 Subject: [PATCH] frontend-app-api: update instantiateAppNodeTree to inline support for v1 extensions Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.test.ts | 459 +++++++++--------- .../src/tree/instantiateAppNodeTree.ts | 33 +- 2 files changed, 262 insertions(+), 230 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index aded5d7826..b8acb6643d 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -15,14 +15,15 @@ */ import { + AnyExtensionDataRef, AppNode, Extension, ExtensionInput, + PortableSchema, ResolvedExtensionInput, createExtension, createExtensionDataRef, createExtensionInput, - createSchemaFromZod, } from '@backstage/frontend-plugin-api'; import { createAppNodeInstance, @@ -31,7 +32,12 @@ import { import { AppNodeSpec } from '@backstage/frontend-plugin-api'; import { resolveAppTree } from './resolveAppTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { + InternalExtension, + resolveExtensionDefinition, +} from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { createSchemaFromZod } from '../../../frontend-plugin-api/src/schema/createSchemaFromZod'; import { withLogCollector } from '@backstage/test-utils'; const testDataRef = createExtensionDataRef().with({ id: 'test' }); @@ -80,28 +86,63 @@ function makeInstanceWithId( }; } +function createV1ExtensionInput( + extensionData: Record, + options: { singleton?: boolean; optional?: boolean } = {}, +) { + return { + $$type: '@backstage/ExtensionInput' as const, + extensionData, + config: { + singleton: options.singleton ?? false, + optional: options.optional ?? false, + }, + }; +} + +function createV1Extension(opts: { + namespace: string; + name?: string; + attachTo?: { id: string; input: string }; + inputs?: Record>; + output: Record; + configSchema?: PortableSchema; + factory: (ctx: { inputs: any; config: any }) => any; +}): Extension { + const ext: InternalExtension = { + $$type: '@backstage/Extension', + version: 'v1', + id: opts.name ? `${opts.namespace}/${opts.name}` : opts.namespace, + disabled: false, + attachTo: opts.attachTo ?? { id: 'ignored', input: 'ignored' }, + inputs: opts.inputs ?? {}, + output: opts.output, + configSchema: opts.configSchema, + factory: opts.factory, + }; + return ext; +} + describe('instantiateAppNodeTree', () => { describe('v1', () => { - const simpleExtension = resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - other: otherDataRef.optional(), - }, - configSchema: createSchemaFromZod(z => - z.object({ - output: z.string().default('test'), - other: z.number().optional(), - }), - ), - factory({ config }) { - return { test: config.output, other: config.other }; - }, - }), - ); + const simpleExtension = createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + other: otherDataRef.optional(), + }, + configSchema: createSchemaFromZod(z => + z.object({ + output: z.string().default('test'), + other: z.number().optional(), + }), + ), + factory({ config }) { + return { test: config.output, other: config.other }; + }, + }); it('should instantiate a single node', () => { const tree = resolveAppTree('root-node', [ @@ -129,21 +170,19 @@ describe('instantiateAppNodeTree', () => { it('should instantiate a node with attachments', () => { const tree = resolveAppTree('root-node', [ makeSpec( - resolveExtensionDefinition( - createExtension({ - namespace: 'root-node', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createExtensionInput({ test: testDataRef }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ inputs }) { - return { inputMirror: inputs }; - }, - }), - ), + createV1Extension({ + namespace: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createV1ExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ inputs }) { + return { inputMirror: inputs }; + }, + }), ), makeSpec(simpleExtension, { id: 'child-node', @@ -175,21 +214,19 @@ describe('instantiateAppNodeTree', () => { const tree = resolveAppTree('root-node', [ { ...makeSpec( - resolveExtensionDefinition( - createExtension({ - namespace: 'root-node', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createExtensionInput({ test: testDataRef }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ inputs }) { - return { inputMirror: inputs }; - }, - }), - ), + createV1Extension({ + namespace: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createV1ExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ inputs }) { + return { inputMirror: inputs }; + }, + }), ), }, { @@ -262,46 +299,44 @@ describe('instantiateAppNodeTree', () => { const instance = createAppNodeInstance({ attachments, node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - optionalSingletonPresent: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true, optional: true }, - ), - optionalSingletonMissing: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true, optional: true }, - ), - singleton: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true }, - ), - many: createExtensionInput({ + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + optionalSingletonPresent: createV1ExtensionInput( + { test: testDataRef, other: otherDataRef.optional(), - }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ inputs }) { - return { inputMirror: inputs }; - }, - }), - ), + }, + { singleton: true, optional: true }, + ), + optionalSingletonMissing: createV1ExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true, optional: true }, + ), + singleton: createV1ExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true }, + ), + many: createV1ExtensionInput({ + test: testDataRef, + other: otherDataRef.optional(), + }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ inputs }) { + return { inputMirror: inputs }; + }, + }), ), }); @@ -344,19 +379,17 @@ describe('instantiateAppNodeTree', () => { expect(() => createAppNodeInstance({ node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory() { - const error = new Error('NOPE'); - error.name = 'NopeError'; - throw error; - }, - }), - ), + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, + }), ), attachments: new Map(), }), @@ -369,20 +402,18 @@ describe('instantiateAppNodeTree', () => { expect(() => createAppNodeInstance({ node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test1: testDataRef, - test2: testDataRef, - }, - factory({}) { - return { test1: 'test', test2: 'test2' }; - }, - }), - ), + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test1: testDataRef, + test2: testDataRef, + }, + factory({}) { + return { test1: 'test', test2: 'test2' }; + }, + }), ), attachments: new Map(), }), @@ -395,19 +426,17 @@ describe('instantiateAppNodeTree', () => { expect(() => createAppNodeInstance({ node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - }, - factory({}) { - return { nonexistent: 'test' } as any; - }, - }), - ), + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + }, + factory({}) { + return { nonexistent: 'test' } as any; + }, + }), ), attachments: new Map(), }), @@ -420,23 +449,21 @@ describe('instantiateAppNodeTree', () => { expect(() => createAppNodeInstance({ node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), ), attachments: new Map(), }), @@ -467,20 +494,18 @@ describe('instantiateAppNodeTree', () => { ], ]), node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'parent', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - declared: createExtensionInput({ - test: testDataRef, - }), - }, - output: {}, - factory: () => ({}), - }), - ), + createV1Extension({ + namespace: 'app', + name: 'parent', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + declared: createV1ExtensionInput({ + test: testDataRef, + }), + }, + output: {}, + factory: () => ({}), + }), ), }), ); @@ -507,15 +532,13 @@ describe('instantiateAppNodeTree', () => { ], ]), node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'parent', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory: () => ({}), - }), - ), + createV1Extension({ + namespace: 'app', + name: 'parent', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory: () => ({}), + }), ), }), ); @@ -539,23 +562,21 @@ describe('instantiateAppNodeTree', () => { ], ]), node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), ), }), ).toThrow( @@ -576,23 +597,21 @@ describe('instantiateAppNodeTree', () => { ], ]), node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, - }, - { singleton: true, optional: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true, optional: true }, + ), + }, + output: {}, + factory: () => ({}), + }), ), }), ).toThrow( @@ -607,23 +626,21 @@ describe('instantiateAppNodeTree', () => { ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], ]), node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - other: otherDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + other: otherDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), ), }), ).toThrowErrorMatchingInlineSnapshot( diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index c70e351105..fd8d324652 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -15,9 +15,7 @@ */ import { - AnyExtensionDataMap, AnyExtensionDataRef, - AnyExtensionInputMap, ExtensionDataContainer, ExtensionDataRef, ExtensionInput, @@ -32,8 +30,10 @@ type Mutable = { -readonly [P in keyof T]: T[P]; }; -function resolveInputDataMap( - dataMap: AnyExtensionDataMap, +function resolveV1InputDataMap( + dataMap: { + [name in string]: AnyExtensionDataRef; + }, attachment: AppNode, inputName: string, ) { @@ -134,9 +134,17 @@ function reportUndeclaredAttachments( } function resolveV1Inputs( - inputMap: AnyExtensionInputMap, + inputMap: { + [inputName in string]: { + $$type: '@backstage/ExtensionInput'; + extensionData: { + [name in string]: AnyExtensionDataRef; + }; + config: { optional: boolean; singleton: boolean }; + }; + }, attachments: ReadonlyMap, -): ResolvedExtensionInputs { +) { return mapValues(inputMap, (input, inputName) => { const attachedNodes = attachments.get(inputName) ?? []; @@ -158,7 +166,7 @@ function resolveV1Inputs( } return { node: attachedNodes[0], - output: resolveInputDataMap( + output: resolveV1InputDataMap( input.extensionData, attachedNodes[0], inputName, @@ -168,9 +176,16 @@ function resolveV1Inputs( return attachedNodes.map(attachment => ({ node: attachment, - output: resolveInputDataMap(input.extensionData, attachment, inputName), + output: resolveV1InputDataMap(input.extensionData, attachment, inputName), })); - }) as ResolvedExtensionInputs; + }) as { + [inputName in string]: { + node: AppNode; + output: { + [name in string]: unknown; + }; + }; + }; } function resolveV2Inputs(