Merge pull request #25976 from backstage/rugvip/inputs
frontend-plugin-api: add support for overriding inputs values
This commit is contained in:
@@ -92,6 +92,16 @@ function resolveInputDataContainer(
|
||||
get(ref) {
|
||||
return dataMap.get(ref.id);
|
||||
},
|
||||
*[Symbol.iterator]() {
|
||||
for (const [id, value] of dataMap) {
|
||||
// TODO: Would be better to be able to create a new instance using the ref here instead
|
||||
yield {
|
||||
$$type: '@backstage/ExtensionDataValue',
|
||||
id,
|
||||
value,
|
||||
};
|
||||
}
|
||||
},
|
||||
} as { node: AppNode } & ExtensionDataContainer<AnyExtensionDataRef>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1281,7 +1281,7 @@ export interface ExtensionBlueprint<
|
||||
params: TParams,
|
||||
context?: {
|
||||
config?: TConfig;
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides<TInputs>;
|
||||
},
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
@@ -1470,7 +1470,7 @@ export interface ExtensionDefinition<
|
||||
factory(
|
||||
originalFactory: (context?: {
|
||||
config?: TConfig;
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides<TInputs>;
|
||||
}) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
@@ -1868,6 +1868,73 @@ export type ResolvedExtensionInputs<
|
||||
: Expand<ResolvedExtensionInput<TInputs[InputName]> | undefined>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ResolveInputValueOverrides<
|
||||
TInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>;
|
||||
} = {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>;
|
||||
},
|
||||
> = Expand<
|
||||
{
|
||||
[KName in keyof TInputs as TInputs[KName] extends ExtensionInput<
|
||||
any,
|
||||
{
|
||||
optional: infer IOptional extends boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>
|
||||
? IOptional extends true
|
||||
? never
|
||||
: KName
|
||||
: never]: TInputs[KName] extends ExtensionInput<
|
||||
infer IDataRefs,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: infer ISingleton extends boolean;
|
||||
}
|
||||
>
|
||||
? ISingleton extends true
|
||||
? Iterable<ExtensionDataRefToValue<IDataRefs>>
|
||||
: Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>
|
||||
: never;
|
||||
} & {
|
||||
[KName in keyof TInputs as TInputs[KName] extends ExtensionInput<
|
||||
any,
|
||||
{
|
||||
optional: infer IOptional extends boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>
|
||||
? IOptional extends true
|
||||
? KName
|
||||
: never
|
||||
: never]?: TInputs[KName] extends ExtensionInput<
|
||||
infer IDataRefs,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: infer ISingleton extends boolean;
|
||||
}
|
||||
>
|
||||
? ISingleton extends true
|
||||
? Iterable<ExtensionDataRefToValue<IDataRefs>>
|
||||
: Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>
|
||||
: never;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type RouteFunc<TParams extends AnyRouteRefParams> = (
|
||||
...[params]: TParams extends undefined
|
||||
|
||||
@@ -732,5 +732,297 @@ describe('createExtension', () => {
|
||||
}).data(stringDataRef),
|
||||
).toBe('foo-hello-override-world');
|
||||
});
|
||||
|
||||
it('should be able to override input values', () => {
|
||||
const outputRef = createExtensionDataRef<unknown>().with({
|
||||
id: 'output',
|
||||
});
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({
|
||||
id: 'test1',
|
||||
});
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({
|
||||
id: 'test2',
|
||||
});
|
||||
|
||||
const subject = createExtension({
|
||||
name: 'subject',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
opt: createExtensionInput([testDataRef1.optional()], {
|
||||
singleton: true,
|
||||
optional: true,
|
||||
}),
|
||||
single: createExtensionInput(
|
||||
[testDataRef1, testDataRef2.optional()],
|
||||
{
|
||||
singleton: true,
|
||||
},
|
||||
),
|
||||
multi: createExtensionInput([testDataRef1]),
|
||||
},
|
||||
output: [outputRef],
|
||||
factory({ inputs }) {
|
||||
return [
|
||||
outputRef({
|
||||
opt: inputs.opt?.get(testDataRef1) ?? 'none',
|
||||
single: inputs.single.get(testDataRef1),
|
||||
singleOpt: inputs.single.get(testDataRef2) ?? 'none',
|
||||
multi: inputs.multi.map(i => i.get(testDataRef1)).join(','),
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const optExt = createExtension({
|
||||
name: 'opt',
|
||||
attachTo: { id: 'subject', input: 'opt' },
|
||||
output: [testDataRef1],
|
||||
factory: () => [testDataRef1('orig-opt')],
|
||||
});
|
||||
|
||||
const singleExt = createExtension({
|
||||
name: 'single',
|
||||
attachTo: { id: 'subject', input: 'single' },
|
||||
output: [testDataRef1, testDataRef2.optional()],
|
||||
factory: () => [testDataRef1('orig-single')],
|
||||
});
|
||||
|
||||
const multi1Ext = createExtension({
|
||||
name: 'multi1',
|
||||
attachTo: { id: 'subject', input: 'multi' },
|
||||
output: [testDataRef1],
|
||||
factory: () => [testDataRef1('orig-multi1')],
|
||||
});
|
||||
|
||||
const multi2Ext = createExtension({
|
||||
name: 'multi2',
|
||||
attachTo: { id: 'subject', input: 'multi' },
|
||||
output: [testDataRef1],
|
||||
factory: () => [testDataRef1('orig-multi2')],
|
||||
});
|
||||
|
||||
expect(
|
||||
createExtensionTester(subject)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
singleOpt: 'none',
|
||||
multi: 'orig-multi1,orig-multi2',
|
||||
});
|
||||
|
||||
// All values provided
|
||||
expect(
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory) {
|
||||
return originalFactory({
|
||||
inputs: {
|
||||
opt: [testDataRef1('opt')],
|
||||
single: [testDataRef1('single'), testDataRef2('singleOpt')],
|
||||
multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]],
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toEqual({
|
||||
opt: 'opt',
|
||||
single: 'single',
|
||||
singleOpt: 'singleOpt',
|
||||
multi: 'multi1,multi2',
|
||||
});
|
||||
|
||||
// Minimal values provided
|
||||
expect(
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory) {
|
||||
return originalFactory({
|
||||
inputs: {
|
||||
single: [testDataRef1('single')],
|
||||
multi: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toEqual({
|
||||
opt: 'none',
|
||||
single: 'single',
|
||||
singleOpt: 'none',
|
||||
multi: '',
|
||||
});
|
||||
|
||||
// Forward inputs directly
|
||||
expect(
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory, { inputs }) {
|
||||
return originalFactory({
|
||||
inputs,
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
singleOpt: 'none',
|
||||
multi: 'orig-multi1,orig-multi2',
|
||||
});
|
||||
|
||||
// Forward inputs separately
|
||||
expect(
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory, { inputs }) {
|
||||
return originalFactory({
|
||||
inputs: {
|
||||
opt: inputs.opt,
|
||||
single: inputs.single,
|
||||
multi: inputs.multi,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
singleOpt: 'none',
|
||||
multi: 'orig-multi1,orig-multi2',
|
||||
});
|
||||
|
||||
// Overriding based on original input
|
||||
expect(
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory, { inputs }) {
|
||||
return originalFactory({
|
||||
inputs: {
|
||||
single: [
|
||||
testDataRef1(`override-${inputs.single.get(testDataRef1)}`),
|
||||
testDataRef2('new-singleOpt'),
|
||||
],
|
||||
multi: inputs.multi.map(i => [
|
||||
testDataRef1(`override-${i.get(testDataRef1)}`),
|
||||
]),
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toEqual({
|
||||
opt: 'none',
|
||||
single: 'override-orig-single',
|
||||
singleOpt: 'new-singleOpt',
|
||||
multi: 'override-orig-multi1,override-orig-multi2',
|
||||
});
|
||||
|
||||
// Mismatched input override length
|
||||
expect(() =>
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory, { inputs }) {
|
||||
return originalFactory({
|
||||
inputs: {
|
||||
...inputs,
|
||||
multi: [[testDataRef1('multi1')]],
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`,
|
||||
);
|
||||
|
||||
// Required input not provided
|
||||
expect(() =>
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory, { inputs }) {
|
||||
return originalFactory({
|
||||
inputs: {
|
||||
...inputs,
|
||||
single: [testDataRef2('singleOpt')],
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`,
|
||||
);
|
||||
|
||||
// Wrong value provided
|
||||
expect(() =>
|
||||
createExtensionTester(
|
||||
subject.override({
|
||||
factory(originalFactory) {
|
||||
return originalFactory({
|
||||
inputs: {
|
||||
// @ts-expect-error
|
||||
opt: [testDataRef2('opt')],
|
||||
// @ts-expect-error
|
||||
single: [testDataRef1('single'), outputRef({})],
|
||||
multi: [
|
||||
// @ts-expect-error
|
||||
[testDataRef2('multi1')],
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.add(optExt)
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
import { AppNode } from '../apis';
|
||||
import { PortableSchema, createSchemaFromZod } from '../schema';
|
||||
import { Expand } from '../types';
|
||||
import { createDataContainer } from './createExtensionBlueprint';
|
||||
import {
|
||||
ResolveInputValueOverrides,
|
||||
createDataContainer,
|
||||
resolveInputOverrides,
|
||||
} from './createExtensionBlueprint';
|
||||
import {
|
||||
AnyExtensionDataRef,
|
||||
ExtensionDataRef,
|
||||
@@ -259,7 +263,7 @@ export interface ExtensionDefinition<
|
||||
factory(
|
||||
originalFactory: (context?: {
|
||||
config?: TConfig;
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides<TInputs>;
|
||||
}) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
@@ -558,7 +562,7 @@ export function createExtension<
|
||||
ReturnType<TConfigSchema[key]>
|
||||
>;
|
||||
};
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides<TInputs>;
|
||||
}) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
@@ -646,14 +650,19 @@ export function createExtension<
|
||||
ReturnType<TConfigSchema[key]>
|
||||
>;
|
||||
};
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides<TInputs>;
|
||||
}): ExtensionDataContainer<UOutput> => {
|
||||
return createDataContainer<UOutput>(
|
||||
newOptions.factory({
|
||||
node,
|
||||
config: innerContext?.config ?? config,
|
||||
inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden
|
||||
inputs: resolveInputOverrides(
|
||||
newOptions.inputs,
|
||||
inputs,
|
||||
innerContext?.inputs,
|
||||
) as any, // TODO: Might be able to improve this once legacy inputs are gone
|
||||
}) as Iterable<any>,
|
||||
newOptions.output,
|
||||
);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,9 +16,15 @@
|
||||
|
||||
import React from 'react';
|
||||
import { coreExtensionData } from './coreExtensionData';
|
||||
import { createExtensionBlueprint } from './createExtensionBlueprint';
|
||||
import {
|
||||
createDataContainer,
|
||||
createExtensionBlueprint,
|
||||
} from './createExtensionBlueprint';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import {
|
||||
ExtensionDataValue,
|
||||
createExtensionDataRef,
|
||||
} from './createExtensionDataRef';
|
||||
import { createExtensionInput } from './createExtensionInput';
|
||||
import { RouteRef } from '../routing';
|
||||
import {
|
||||
@@ -28,12 +34,15 @@ import {
|
||||
|
||||
function unused(..._any: any[]) {}
|
||||
|
||||
function factoryOutput(ext: ExtensionDefinition<any, any>) {
|
||||
function factoryOutput(
|
||||
ext: ExtensionDefinition<any, any>,
|
||||
inputs: unknown = undefined,
|
||||
) {
|
||||
const int = toInternalExtensionDefinition(ext);
|
||||
if (int.version !== 'v2') {
|
||||
throw new Error('Expected v2 extension');
|
||||
}
|
||||
return Array.from(int.factory({} as any));
|
||||
return Array.from(int.factory({ inputs } as any));
|
||||
}
|
||||
|
||||
describe('createExtensionBlueprint', () => {
|
||||
@@ -335,6 +344,257 @@ describe('createExtensionBlueprint', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to override inputs when calling original factory', () => {
|
||||
const outputRef = createExtensionDataRef<unknown>().with({ id: 'output' });
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({ id: 'test1' });
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({ id: 'test2' });
|
||||
|
||||
const Blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
inputs: {
|
||||
opt: createExtensionInput([testDataRef1.optional()], {
|
||||
singleton: true,
|
||||
optional: true,
|
||||
}),
|
||||
single: createExtensionInput([testDataRef1, testDataRef2.optional()], {
|
||||
singleton: true,
|
||||
}),
|
||||
multi: createExtensionInput([testDataRef1]),
|
||||
},
|
||||
output: [outputRef],
|
||||
factory(_, { inputs }) {
|
||||
return [
|
||||
outputRef({
|
||||
opt: inputs.opt?.get(testDataRef1) ?? 'none',
|
||||
single: inputs.single.get(testDataRef1),
|
||||
singleOpt: inputs.single.get(testDataRef2) ?? 'none',
|
||||
multi: inputs.multi.map(i => i.get(testDataRef1)).join(','),
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const mockInput = (node: string, ...data: ExtensionDataValue<any, any>[]) =>
|
||||
Object.assign(createDataContainer(data), {
|
||||
node,
|
||||
});
|
||||
const mockParentInputs = {
|
||||
opt: mockInput('node-opt', testDataRef1('orig-opt')),
|
||||
single: mockInput('node-single', testDataRef1('orig-single')),
|
||||
multi: [
|
||||
mockInput('node-multi1', testDataRef1('orig-multi1')),
|
||||
mockInput('node-multi2', testDataRef1('orig-multi2')),
|
||||
],
|
||||
};
|
||||
|
||||
// All values provided
|
||||
expect(
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory) {
|
||||
return origFactory(
|
||||
{},
|
||||
{
|
||||
inputs: {
|
||||
opt: [testDataRef1('opt')],
|
||||
single: [testDataRef1('single'), testDataRef2('singleOpt')],
|
||||
multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toEqual([
|
||||
outputRef({
|
||||
opt: 'opt',
|
||||
single: 'single',
|
||||
singleOpt: 'singleOpt',
|
||||
multi: 'multi1,multi2',
|
||||
}),
|
||||
]);
|
||||
|
||||
// Minimal values provided
|
||||
expect(
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory) {
|
||||
return origFactory(
|
||||
{},
|
||||
{
|
||||
inputs: {
|
||||
single: [testDataRef1('single')],
|
||||
multi: [],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toEqual([
|
||||
outputRef({
|
||||
opt: 'none',
|
||||
single: 'single',
|
||||
singleOpt: 'none',
|
||||
multi: '',
|
||||
}),
|
||||
]);
|
||||
|
||||
// Mismatched input override length
|
||||
expect(() =>
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory, { inputs }) {
|
||||
return origFactory(
|
||||
{},
|
||||
{
|
||||
inputs: {
|
||||
...inputs,
|
||||
multi: [[testDataRef1('multi1')]],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"override data provided for input 'multi' must match the length of the original inputs"`,
|
||||
);
|
||||
|
||||
// Required input not provided
|
||||
expect(() =>
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory, { inputs }) {
|
||||
return origFactory(
|
||||
{},
|
||||
{
|
||||
inputs: {
|
||||
...inputs,
|
||||
single: [testDataRef2('singleOpt')],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"missing required extension data value(s) 'test1'"`,
|
||||
);
|
||||
|
||||
// Wrong value provided
|
||||
expect(() =>
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory) {
|
||||
return origFactory(
|
||||
{},
|
||||
{
|
||||
inputs: {
|
||||
// @ts-expect-error
|
||||
opt: [testDataRef2('opt')],
|
||||
// @ts-expect-error
|
||||
single: [testDataRef1('single'), outputRef({})],
|
||||
multi: [
|
||||
// @ts-expect-error
|
||||
[testDataRef2('multi1')],
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"extension data 'test2' was provided but not declared"`,
|
||||
);
|
||||
|
||||
// Forwarding entire inputs object
|
||||
expect(
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory, { inputs }) {
|
||||
return origFactory({}, { inputs });
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toEqual([
|
||||
outputRef({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
singleOpt: 'none',
|
||||
multi: 'orig-multi1,orig-multi2',
|
||||
}),
|
||||
]);
|
||||
|
||||
// Forwarding individual outputs
|
||||
expect(
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory, { inputs }) {
|
||||
return origFactory(
|
||||
{},
|
||||
{
|
||||
inputs: {
|
||||
opt: inputs.opt,
|
||||
single: inputs.single,
|
||||
multi: inputs.multi,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toEqual([
|
||||
outputRef({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
singleOpt: 'none',
|
||||
multi: 'orig-multi1,orig-multi2',
|
||||
}),
|
||||
]);
|
||||
|
||||
// Overriding based on original input
|
||||
expect(
|
||||
factoryOutput(
|
||||
Blueprint.makeWithOverrides({
|
||||
factory(origFactory, { inputs }) {
|
||||
return origFactory(
|
||||
{},
|
||||
{
|
||||
inputs: {
|
||||
single: [
|
||||
testDataRef1(`override-${inputs.single.get(testDataRef1)}`),
|
||||
testDataRef2('new-singleOpt'),
|
||||
],
|
||||
multi: inputs.multi.map(i => [
|
||||
testDataRef1(`override-${i.get(testDataRef1)}`),
|
||||
]),
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
mockParentInputs,
|
||||
),
|
||||
).toEqual([
|
||||
outputRef({
|
||||
opt: 'none',
|
||||
single: 'override-orig-single',
|
||||
singleOpt: 'new-singleOpt',
|
||||
multi: 'override-orig-multi1,override-orig-multi2',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow merging of inputs', () => {
|
||||
const blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
@@ -429,6 +689,43 @@ describe('createExtensionBlueprint', () => {
|
||||
expect(factoryOutput(ext)).toEqual([testDataRef2('foobar')]);
|
||||
});
|
||||
|
||||
it('should reject invalid output from original factory', () => {
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({ id: 'test1' });
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({ id: 'test2' });
|
||||
|
||||
expect(() =>
|
||||
factoryOutput(
|
||||
// @ts-expect-error
|
||||
createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
output: [testDataRef1],
|
||||
factory() {
|
||||
return [testDataRef2('foo')];
|
||||
},
|
||||
}).makeWithOverrides({ factory: orig => orig({}) }),
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"extension data 'test2' was provided but not declared"`,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
factoryOutput(
|
||||
// @ts-expect-error
|
||||
createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
output: [testDataRef1],
|
||||
factory() {
|
||||
return [];
|
||||
},
|
||||
}).makeWithOverrides({ factory: orig => orig({}) }),
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"missing required extension data value(s) 'test1'"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow returning of the parent data container', () => {
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({ id: 'test1' });
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({ id: 'test2' });
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CreateExtensionOptions,
|
||||
ExtensionDataContainer,
|
||||
ExtensionDefinition,
|
||||
ResolvedExtensionInput,
|
||||
ResolvedExtensionInputs,
|
||||
VerifyExtensionFactoryOutput,
|
||||
createExtension,
|
||||
@@ -29,6 +30,7 @@ import { ExtensionInput } from './createExtensionInput';
|
||||
import {
|
||||
AnyExtensionDataRef,
|
||||
ExtensionDataRef,
|
||||
ExtensionDataRefToValue,
|
||||
ExtensionDataValue,
|
||||
} from './createExtensionDataRef';
|
||||
|
||||
@@ -75,6 +77,61 @@ export type CreateExtensionBlueprintOptions<
|
||||
dataRefs?: TDataRefs;
|
||||
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>;
|
||||
|
||||
/** @public */
|
||||
export type ResolveInputValueOverrides<
|
||||
TInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
} = {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
> = Expand<
|
||||
{
|
||||
[KName in keyof TInputs as TInputs[KName] extends ExtensionInput<
|
||||
any,
|
||||
{
|
||||
optional: infer IOptional extends boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>
|
||||
? IOptional extends true
|
||||
? never
|
||||
: KName
|
||||
: never]: TInputs[KName] extends ExtensionInput<
|
||||
infer IDataRefs,
|
||||
{ optional: boolean; singleton: infer ISingleton extends boolean }
|
||||
>
|
||||
? ISingleton extends true
|
||||
? Iterable<ExtensionDataRefToValue<IDataRefs>>
|
||||
: Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>
|
||||
: never;
|
||||
} & {
|
||||
[KName in keyof TInputs as TInputs[KName] extends ExtensionInput<
|
||||
any,
|
||||
{
|
||||
optional: infer IOptional extends boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>
|
||||
? IOptional extends true
|
||||
? KName
|
||||
: never
|
||||
: never]?: TInputs[KName] extends ExtensionInput<
|
||||
infer IDataRefs,
|
||||
{ optional: boolean; singleton: infer ISingleton extends boolean }
|
||||
>
|
||||
? ISingleton extends true
|
||||
? Iterable<ExtensionDataRefToValue<IDataRefs>>
|
||||
: Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>
|
||||
: never;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -156,7 +213,7 @@ export interface ExtensionBlueprint<
|
||||
params: TParams,
|
||||
context?: {
|
||||
config?: TConfig;
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides<TInputs>;
|
||||
},
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
@@ -202,13 +259,34 @@ export function createDataContainer<UData extends AnyExtensionDataRef>(
|
||||
? ExtensionDataValue<IData, IId>
|
||||
: never
|
||||
>,
|
||||
declaredRefs?: ExtensionDataRef<any, any, any>[],
|
||||
): ExtensionDataContainer<UData> {
|
||||
const container = new Map<string, ExtensionDataValue<any, any>>();
|
||||
const verifyRefs =
|
||||
declaredRefs && new Map(declaredRefs.map(ref => [ref.id, ref]));
|
||||
|
||||
for (const output of values) {
|
||||
if (verifyRefs) {
|
||||
if (!verifyRefs.delete(output.id)) {
|
||||
throw new Error(
|
||||
`extension data '${output.id}' was provided but not declared`,
|
||||
);
|
||||
}
|
||||
}
|
||||
container.set(output.id, output);
|
||||
}
|
||||
|
||||
const remainingRefs =
|
||||
verifyRefs &&
|
||||
Array.from(verifyRefs.values()).filter(ref => !ref.config.optional);
|
||||
if (remainingRefs && remainingRefs.length > 0) {
|
||||
throw new Error(
|
||||
`missing required extension data value(s) '${remainingRefs
|
||||
.map(ref => ref.id)
|
||||
.join(', ')}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
get(ref) {
|
||||
return container.get(ref.id)?.value;
|
||||
@@ -219,6 +297,84 @@ export function createDataContainer<UData extends AnyExtensionDataRef>(
|
||||
} as ExtensionDataContainer<UData>;
|
||||
}
|
||||
|
||||
function expectArray<T>(value: T | T[]): T[] {
|
||||
return value as T[];
|
||||
}
|
||||
function expectItem<T>(value: T | T[]): T {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function resolveInputOverrides(
|
||||
declaredInputs?: {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
inputs?: {
|
||||
[KName in string]?:
|
||||
| ({ node: AppNode } & ExtensionDataContainer<any>)
|
||||
| Array<{ node: AppNode } & ExtensionDataContainer<any>>;
|
||||
},
|
||||
inputOverrides?: ResolveInputValueOverrides,
|
||||
) {
|
||||
if (!declaredInputs || !inputs || !inputOverrides) {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
const newInputs: typeof inputs = {};
|
||||
for (const name in declaredInputs) {
|
||||
if (!Object.hasOwn(declaredInputs, name)) {
|
||||
continue;
|
||||
}
|
||||
const declaredInput = declaredInputs[name];
|
||||
const providedData = inputOverrides[name];
|
||||
if (declaredInput.config.singleton) {
|
||||
const originalInput = expectItem(inputs[name]);
|
||||
if (providedData) {
|
||||
const providedContainer = createDataContainer(
|
||||
providedData as Iterable<ExtensionDataValue<any, any>>,
|
||||
declaredInput.extensionData,
|
||||
);
|
||||
if (!originalInput) {
|
||||
throw new Error(
|
||||
`attempted to override data of input '${name}' but it is not present in the original inputs`,
|
||||
);
|
||||
}
|
||||
newInputs[name] = Object.assign(providedContainer, {
|
||||
name: (originalInput as ResolvedExtensionInput<any>).node,
|
||||
}) as any;
|
||||
}
|
||||
} else {
|
||||
const originalInput = expectArray(inputs[name]);
|
||||
if (!Array.isArray(providedData)) {
|
||||
throw new Error(
|
||||
`override data provided for input '${name}' must be an array`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
originalInput.length !== providedData.length &&
|
||||
providedData.length > 0
|
||||
) {
|
||||
throw new Error(
|
||||
`override data provided for input '${name}' must match the length of the original inputs`,
|
||||
);
|
||||
}
|
||||
newInputs[name] = providedData.map((data, i) => {
|
||||
const providedContainer = createDataContainer(
|
||||
data as Iterable<ExtensionDataValue<any, any>>,
|
||||
declaredInput.extensionData,
|
||||
);
|
||||
return Object.assign(providedContainer, {
|
||||
name: (originalInput[i] as ResolvedExtensionInput<any>).node,
|
||||
}) as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
return newInputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -288,7 +444,7 @@ class ExtensionBlueprintImpl<
|
||||
ReturnType<TConfigSchema[key]>
|
||||
>;
|
||||
};
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides<TInputs>;
|
||||
},
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
@@ -347,15 +503,20 @@ class ExtensionBlueprintImpl<
|
||||
ReturnType<TConfigSchema[key]>
|
||||
>;
|
||||
};
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs?: ResolveInputValueOverrides;
|
||||
},
|
||||
): ExtensionDataContainer<UOutput> => {
|
||||
return createDataContainer<UOutput>(
|
||||
this.options.factory(innerParams, {
|
||||
node,
|
||||
config: innerContext?.config ?? config,
|
||||
inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden
|
||||
inputs: resolveInputOverrides(
|
||||
this.options.inputs,
|
||||
inputs,
|
||||
innerContext?.inputs,
|
||||
) as any, // TODO: Might be able to improve this once legacy inputs are gone
|
||||
}),
|
||||
this.options.output,
|
||||
);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -56,6 +56,7 @@ export {
|
||||
} from './types';
|
||||
export {
|
||||
type CreateExtensionBlueprintOptions,
|
||||
type ResolveInputValueOverrides,
|
||||
type ExtensionBlueprint,
|
||||
createExtensionBlueprint,
|
||||
} from './createExtensionBlueprint';
|
||||
|
||||
Reference in New Issue
Block a user