frontend-plugin-api: add support for overriding input values when overriding extensions

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-08-11 19:57:42 +02:00
parent 66c6bbcef5
commit cea81165f4
3 changed files with 307 additions and 6 deletions
@@ -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: [[testDataRef1('multi1')], [testDataRef1('multi2')]],
},
});
},
}),
)
.add(optExt)
.add(singleExt)
.add(multi1Ext)
.add(multi2Ext)
.data(outputRef),
).toEqual({
opt: 'none',
single: 'single',
singleOpt: 'none',
multi: 'multi1,multi2',
});
// 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', Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`,
);
// 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 data values for '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', Invalid data value provided, 'test2' was 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,
);
},
{
@@ -78,7 +78,7 @@ export type CreateExtensionBlueprintOptions<
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>;
/** @ignore */
type ResolveInputValueOverrides<
export type ResolveInputValueOverrides<
TInputs extends {
[inputName in string]: ExtensionInput<
AnyExtensionDataRef,