feat: added input types for the config to the extensions
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -29,7 +29,7 @@ import { BackstagePlugin, Extension, ExtensionDataRef } from '../../wiring';
|
||||
export interface AppNodeSpec {
|
||||
readonly id: string;
|
||||
readonly attachTo: { id: string; input: string };
|
||||
readonly extension: Extension<unknown>;
|
||||
readonly extension: Extension<unknown, unknown>;
|
||||
readonly disabled: boolean;
|
||||
readonly config?: unknown;
|
||||
readonly source?: BackstagePlugin;
|
||||
|
||||
@@ -28,6 +28,12 @@ export const IconBundleBlueprint = createExtensionBlueprint({
|
||||
output: {
|
||||
icons: iconsDataRef,
|
||||
},
|
||||
config: {
|
||||
schema: {
|
||||
icons: z => z.string().default('blob'),
|
||||
test: z => z.string(),
|
||||
},
|
||||
},
|
||||
factory: (params: { icons: { [key in string]: IconComponent } }) => params,
|
||||
dataRefs: {
|
||||
icons: iconsDataRef,
|
||||
|
||||
@@ -25,10 +25,19 @@ describe('createSchemaFromZod', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() => parse({ derp: { bar: 'derp' } })).toThrow(
|
||||
expect(() => {
|
||||
// @ts-expect-error
|
||||
return parse({ derp: { bar: 'derp' } });
|
||||
}).toThrow(
|
||||
`Missing required value at 'foo'; Expected number, received string at 'derp.bar'`,
|
||||
);
|
||||
expect(() => parse(undefined)).toThrow(`Missing required value`);
|
||||
expect(() => parse('derp')).toThrow(`Expected object, received string`);
|
||||
expect(() => {
|
||||
// @ts-expect-error
|
||||
return parse(undefined);
|
||||
}).toThrow(`Missing required value`);
|
||||
expect(() => {
|
||||
// @ts-expect-error
|
||||
return parse('derp');
|
||||
}).toThrow(`Expected object, received string`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ import { PortableSchema } from './types';
|
||||
*/
|
||||
export function createSchemaFromZod<TOutput, TInput>(
|
||||
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
|
||||
): PortableSchema<TOutput> {
|
||||
): PortableSchema<TOutput, TInput> {
|
||||
const schema = schemaCreator(z);
|
||||
return {
|
||||
// TODO: Types allow z.array etc here but it will break stuff
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
/** @public */
|
||||
export type PortableSchema<TOutput> = {
|
||||
parse: (input: unknown) => TOutput;
|
||||
export type PortableSchema<TOutput, TInput = TOutput> = {
|
||||
parse: (input: TInput) => TOutput;
|
||||
schema: JsonObject;
|
||||
};
|
||||
|
||||
@@ -334,6 +334,7 @@ describe('createExtension', () => {
|
||||
foo: 'x',
|
||||
bar: 'y',
|
||||
baz: 'z',
|
||||
// @ts-expect-error
|
||||
qux: 'w',
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -349,8 +350,9 @@ describe('createExtension', () => {
|
||||
foo: 'x',
|
||||
bar: 'bar',
|
||||
});
|
||||
expect(() => extension.configSchema?.parse({})).toThrow(
|
||||
"Missing required value at 'foo'",
|
||||
);
|
||||
expect(() => {
|
||||
// @ts-expect-error
|
||||
return extension.configSchema?.parse({});
|
||||
}).toThrow("Missing required value at 'foo'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface CreateExtensionOptions<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
|
||||
> {
|
||||
kind?: string;
|
||||
@@ -92,33 +93,38 @@ export interface CreateExtensionOptions<
|
||||
inputs?: TInputs;
|
||||
output: TOutput;
|
||||
/** @deprecated - use `config.schema` instead */
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
configSchema?: PortableSchema<TConfig, TConfigInput>;
|
||||
config?: {
|
||||
schema: TConfigSchema;
|
||||
};
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
config: TConfig & {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
};
|
||||
config: TConfig &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<
|
||||
ReturnType<TConfigSchema[key]>
|
||||
>;
|
||||
});
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionDefinition<TConfig> {
|
||||
export interface ExtensionDefinition<TConfig, TConfigInput = TConfig> {
|
||||
$$type: '@backstage/ExtensionDefinition';
|
||||
readonly kind?: string;
|
||||
readonly namespace?: string;
|
||||
readonly name?: string;
|
||||
readonly attachTo: { id: string; input: string };
|
||||
readonly disabled: boolean;
|
||||
readonly configSchema?: PortableSchema<TConfig>;
|
||||
readonly configSchema?: PortableSchema<TConfig, TConfigInput>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalExtensionDefinition<TConfig>
|
||||
extends ExtensionDefinition<TConfig> {
|
||||
export interface InternalExtensionDefinition<TConfig, TConfigInput>
|
||||
extends ExtensionDefinition<TConfig, TConfigInput> {
|
||||
readonly version: 'v1';
|
||||
readonly inputs: AnyExtensionInputMap;
|
||||
readonly output: AnyExtensionDataMap;
|
||||
@@ -130,10 +136,13 @@ export interface InternalExtensionDefinition<TConfig>
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalExtensionDefinition<TConfig>(
|
||||
overrides: ExtensionDefinition<TConfig>,
|
||||
): InternalExtensionDefinition<TConfig> {
|
||||
const internal = overrides as InternalExtensionDefinition<TConfig>;
|
||||
export function toInternalExtensionDefinition<TConfig, TConfigInput>(
|
||||
overrides: ExtensionDefinition<TConfig, TConfigInput>,
|
||||
): InternalExtensionDefinition<TConfig, TConfigInput> {
|
||||
const internal = overrides as InternalExtensionDefinition<
|
||||
TConfig,
|
||||
TConfigInput
|
||||
>;
|
||||
if (internal.$$type !== '@backstage/ExtensionDefinition') {
|
||||
throw new Error(
|
||||
`Invalid extension definition instance, bad type '${internal.$$type}'`,
|
||||
@@ -152,10 +161,32 @@ export function createExtension<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
|
||||
>(
|
||||
options: CreateExtensionOptions<TOutput, TInputs, TConfig, TConfigSchema>,
|
||||
): ExtensionDefinition<TConfig> {
|
||||
options: CreateExtensionOptions<
|
||||
TOutput,
|
||||
TInputs,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
TConfigSchema
|
||||
>,
|
||||
): ExtensionDefinition<
|
||||
TConfig &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
}),
|
||||
TConfigInput &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>)
|
||||
> {
|
||||
const newConfigSchema = options.config?.schema;
|
||||
if (newConfigSchema && options.configSchema) {
|
||||
throw new Error(`Cannot provide both configSchema and config.schema`);
|
||||
@@ -205,5 +236,22 @@ export function createExtension<
|
||||
parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`);
|
||||
return `ExtensionDefinition{${parts.join(',')}}`;
|
||||
},
|
||||
} as InternalExtensionDefinition<TConfig>;
|
||||
} as InternalExtensionDefinition<
|
||||
TConfig &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<
|
||||
ReturnType<TConfigSchema[key]>
|
||||
>;
|
||||
}),
|
||||
TConfigInput &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>)
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface ExtensionBlueprint<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TConfig extends { [key in string]: unknown },
|
||||
TConfigInput extends { [key in string]: unknown },
|
||||
TDataRefs extends AnyExtensionDataMap,
|
||||
> {
|
||||
dataRefs: TDataRefs;
|
||||
@@ -125,7 +126,15 @@ export interface ExtensionBlueprint<
|
||||
[key in keyof TExtensionConfigSchema]: z.infer<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
>;
|
||||
} & TConfig
|
||||
} & TConfig,
|
||||
z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TExtensionConfigSchema]: ReturnType<
|
||||
TExtensionConfigSchema[key]
|
||||
>;
|
||||
}>
|
||||
> &
|
||||
TConfigInput
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -199,7 +208,18 @@ class ExtensionBlueprintImpl<
|
||||
>;
|
||||
} & {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
}
|
||||
},
|
||||
z.input<
|
||||
z.ZodObject<
|
||||
{
|
||||
[key in keyof TExtensionConfigSchema]: ReturnType<
|
||||
TExtensionConfigSchema[key]
|
||||
>;
|
||||
} & {
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}
|
||||
>
|
||||
>
|
||||
> {
|
||||
const schema = {
|
||||
...this.options.config?.schema,
|
||||
@@ -279,6 +299,13 @@ export function createExtensionBlueprint<
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: { [key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>> },
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>,
|
||||
TDataRefs
|
||||
> {
|
||||
return new ExtensionBlueprintImpl(options) as ExtensionBlueprint<
|
||||
@@ -290,6 +317,13 @@ export function createExtensionBlueprint<
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
},
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>,
|
||||
TDataRefs
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ExtensionOverrides, FeatureFlagConfig } from './types';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionOverridesOptions {
|
||||
extensions: ExtensionDefinition<unknown>[];
|
||||
extensions: ExtensionDefinition<any, any>[];
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface PluginOptions<
|
||||
id: string;
|
||||
routes?: Routes;
|
||||
externalRoutes?: ExternalRoutes;
|
||||
extensions?: ExtensionDefinition<unknown>[];
|
||||
extensions?: ExtensionDefinition<any, any>[];
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
|
||||
@@ -26,16 +26,17 @@ import {
|
||||
import { PortableSchema } from '../schema';
|
||||
|
||||
/** @public */
|
||||
export interface Extension<TConfig> {
|
||||
export interface Extension<TConfig, TConfigInput = TConfig> {
|
||||
$$type: '@backstage/Extension';
|
||||
readonly id: string;
|
||||
readonly attachTo: { id: string; input: string };
|
||||
readonly disabled: boolean;
|
||||
readonly configSchema?: PortableSchema<TConfig>;
|
||||
readonly configSchema?: PortableSchema<TConfig, TConfigInput>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalExtension<TConfig> extends Extension<TConfig> {
|
||||
export interface InternalExtension<TConfig, TConfigInput>
|
||||
extends Extension<TConfig, TConfigInput> {
|
||||
readonly version: 'v1';
|
||||
readonly inputs: AnyExtensionInputMap;
|
||||
readonly output: AnyExtensionDataMap;
|
||||
@@ -47,10 +48,10 @@ export interface InternalExtension<TConfig> extends Extension<TConfig> {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalExtension<TConfig>(
|
||||
overrides: Extension<TConfig>,
|
||||
): InternalExtension<TConfig> {
|
||||
const internal = overrides as InternalExtension<TConfig>;
|
||||
export function toInternalExtension<TConfig, TConfigInput>(
|
||||
overrides: Extension<TConfig, TConfigInput>,
|
||||
): InternalExtension<TConfig, TConfigInput> {
|
||||
const internal = overrides as InternalExtension<TConfig, TConfigInput>;
|
||||
if (internal.$$type !== '@backstage/Extension') {
|
||||
throw new Error(
|
||||
`Invalid extension instance, bad type '${internal.$$type}'`,
|
||||
@@ -65,10 +66,10 @@ export function toInternalExtension<TConfig>(
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function resolveExtensionDefinition<TConfig>(
|
||||
definition: ExtensionDefinition<TConfig>,
|
||||
export function resolveExtensionDefinition<TConfig, TConfigInput>(
|
||||
definition: ExtensionDefinition<TConfig, TConfigInput>,
|
||||
context?: { namespace?: string },
|
||||
): Extension<TConfig> {
|
||||
): Extension<TConfig, TConfigInput> {
|
||||
const internalDefinition = toInternalExtensionDefinition(definition);
|
||||
const { name, kind, namespace: _, ...rest } = internalDefinition;
|
||||
const namespace = internalDefinition.namespace ?? context?.namespace;
|
||||
@@ -91,5 +92,5 @@ export function resolveExtensionDefinition<TConfig>(
|
||||
toString() {
|
||||
return `Extension{id=${id}}`;
|
||||
},
|
||||
} as Extension<TConfig>;
|
||||
} as Extension<TConfig, TConfigInput>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user