frontend-plugin-api: add support for extension configuration schema
Co-authored-by: Camila Belo <camilaibs@gmail.com> Co-authored-by: Philipp Hugenroth <philipph@spotify.com> Co-authored-by: Fredrik Adelöw <freben@gmail.com> Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com> Co-authored-by: Johan Haals <johan.haals@gmail.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -4,6 +4,10 @@
|
||||
|
||||
```ts
|
||||
import { ComponentType } from 'react';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { z } from 'zod';
|
||||
import { ZodSchema } from 'zod';
|
||||
import { ZodTypeDef } from 'zod';
|
||||
|
||||
// @public (undocumented)
|
||||
export type AnyExtensionDataMap = Record<string, ExtensionDataRef<any>>;
|
||||
@@ -41,7 +45,8 @@ export function createExtension<
|
||||
extensionData: AnyExtensionDataMap;
|
||||
}
|
||||
>,
|
||||
>(options: CreateExtensionOptions<TData, TPoint>): Extension;
|
||||
TConfig = never,
|
||||
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface CreateExtensionOptions<
|
||||
@@ -52,11 +57,14 @@ export interface CreateExtensionOptions<
|
||||
extensionData: AnyExtensionDataMap;
|
||||
}
|
||||
>,
|
||||
TConfig,
|
||||
> {
|
||||
// (undocumented)
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
bind: ExtensionDataBind<TData>;
|
||||
config?: unknown;
|
||||
config: TConfig;
|
||||
inputs: {
|
||||
[pointName in keyof TPoint]: ExtensionDataValue<
|
||||
TPoint[pointName]['extensionData']
|
||||
@@ -73,13 +81,20 @@ export interface CreateExtensionOptions<
|
||||
export function createPlugin(options: BackstagePluginOptions): BackstagePlugin;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Extension {
|
||||
export function createSchemaFromZod<TOutput, TInput>(
|
||||
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
|
||||
): PortableSchema<TOutput>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Extension<TConfig> {
|
||||
// (undocumented)
|
||||
$$type: 'extension';
|
||||
// (undocumented)
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
bind: ExtensionDataBind<AnyExtensionDataMap>;
|
||||
config?: unknown;
|
||||
config: TConfig;
|
||||
inputs: Record<string, Array<Record<string, unknown>>>;
|
||||
}): void;
|
||||
// (undocumented)
|
||||
@@ -115,10 +130,16 @@ export interface ExtensionInstanceConfig {
|
||||
// (undocumented)
|
||||
at: string;
|
||||
// (undocumented)
|
||||
config: unknown;
|
||||
config?: unknown;
|
||||
// (undocumented)
|
||||
extension: Extension;
|
||||
extension: Extension<unknown>;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type PortableSchema<TOutput> = {
|
||||
parse: (input: unknown) => TOutput;
|
||||
schema: JsonObject;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
"@backstage/types": "workspace:^",
|
||||
"lodash": "^4.17.21",
|
||||
"zod": "^3.21.4",
|
||||
"zod-to-json-schema": "^3.21.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createSchemaFromZod } from './createSchemaFromZod';
|
||||
|
||||
describe('createSchemaFromZod', () => {
|
||||
it('should provide nice parse errors', () => {
|
||||
const { parse } = createSchemaFromZod(z =>
|
||||
z.object({
|
||||
foo: z.union([z.string(), z.number()]),
|
||||
derp: z.object({ bar: z.number() }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() => 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`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { z, ZodSchema, ZodTypeDef } from 'zod';
|
||||
import zodToJsonSchema from 'zod-to-json-schema';
|
||||
|
||||
/** @public */
|
||||
export type PortableSchema<TOutput> = {
|
||||
parse: (input: unknown) => TOutput;
|
||||
schema: JsonObject;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export function createSchemaFromZod<TOutput, TInput>(
|
||||
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
|
||||
): PortableSchema<TOutput> {
|
||||
const schema = schemaCreator(z);
|
||||
return {
|
||||
// TODO: Types allow z.array etc here but it will break stuff
|
||||
parse: input => {
|
||||
const result = schema.safeParse(input);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
throw new Error(result.error.issues.map(formatIssue).join('; '));
|
||||
},
|
||||
// TODO: Verify why we are not compatible with the latest zodToJsonSchema.
|
||||
schema: zodToJsonSchema(schema) as JsonObject,
|
||||
};
|
||||
}
|
||||
|
||||
function formatIssue(issue: z.ZodIssue): string {
|
||||
if (issue.code === 'invalid_union') {
|
||||
return formatIssue(issue.unionErrors[0].issues[0]);
|
||||
}
|
||||
let message = issue.message;
|
||||
if (message === 'Required') {
|
||||
message = `Missing required value`;
|
||||
}
|
||||
if (issue.path.length) {
|
||||
message += ` at '${issue.path.join('.')}'`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
createPlugin,
|
||||
type ExtensionInstanceConfig,
|
||||
type ExtensionInstanceParameters as ExtensionInstanceConfig,
|
||||
type BackstagePlugin,
|
||||
type Extension,
|
||||
type AnyExtensionDataMap,
|
||||
@@ -34,3 +34,7 @@ export {
|
||||
type ExtensionDataRef,
|
||||
type ExtensionDataValue,
|
||||
} from './types';
|
||||
export {
|
||||
createSchemaFromZod,
|
||||
type PortableSchema,
|
||||
} from './createSchemaFromZod';
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { PortableSchema } from './createSchemaFromZod';
|
||||
|
||||
/** @public */
|
||||
export type ExtensionDataRef<T> = {
|
||||
@@ -51,12 +52,14 @@ export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
|
||||
export interface CreateExtensionOptions<
|
||||
TData extends AnyExtensionDataMap,
|
||||
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
|
||||
TConfig,
|
||||
> {
|
||||
inputs?: TPoint;
|
||||
output: TData;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
bind: ExtensionDataBind<TData>;
|
||||
config?: unknown;
|
||||
config: TConfig;
|
||||
inputs: {
|
||||
[pointName in keyof TPoint]: ExtensionDataValue<
|
||||
TPoint[pointName]['extensionData']
|
||||
@@ -66,13 +69,15 @@ export interface CreateExtensionOptions<
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Extension {
|
||||
export interface Extension<TConfig> {
|
||||
$$type: 'extension';
|
||||
// TODO: will extensions have a default "at" as part of their contract, making it optional in the instance config?
|
||||
inputs: Record<string, { extensionData: AnyExtensionDataMap }>;
|
||||
output: AnyExtensionDataMap;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
bind: ExtensionDataBind<AnyExtensionDataMap>;
|
||||
config?: unknown;
|
||||
config: TConfig;
|
||||
inputs: Record<string, Array<Record<string, unknown>>>;
|
||||
}): void;
|
||||
}
|
||||
@@ -81,29 +86,30 @@ export interface Extension {
|
||||
export function createExtension<
|
||||
TData extends AnyExtensionDataMap,
|
||||
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
|
||||
>(options: CreateExtensionOptions<TData, TPoint>): Extension {
|
||||
TConfig = never,
|
||||
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig> {
|
||||
return { ...options, $$type: 'extension', inputs: options.inputs ?? {} };
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionInstanceConfig {
|
||||
export interface ExtensionInstanceParameters {
|
||||
id: string;
|
||||
at: string;
|
||||
extension: Extension;
|
||||
config: unknown;
|
||||
extension: Extension<unknown>;
|
||||
config?: unknown;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface BackstagePluginOptions {
|
||||
id: string;
|
||||
defaultExtensionInstances?: ExtensionInstanceConfig[];
|
||||
defaultExtensionInstances?: ExtensionInstanceParameters[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface BackstagePlugin {
|
||||
$$type: 'backstage-plugin';
|
||||
id: string;
|
||||
defaultExtensionInstances: ExtensionInstanceConfig[];
|
||||
defaultExtensionInstances: ExtensionInstanceParameters[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
||||
Reference in New Issue
Block a user