feat(scaffolder): add first class citizen support for zod
Signed-off-by: Paul Schultz <pschultz@pobox.com> backport better typescript support Signed-off-by: Paul Schultz <pschultz@pobox.com> wip Signed-off-by: Paul Schultz <pschultz@pobox.com> wip Signed-off-by: Paul Schultz <pschultz@pobox.com> complete refactor Signed-off-by: Paul Schultz <pschultz@pobox.com>
This commit is contained in:
committed by
benjdlambert
parent
6bc81c78e7
commit
1a588465e9
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/types': minor
|
||||
---
|
||||
|
||||
feat: feat(scaffolder): add first class citizen support for zod
|
||||
|
||||
Added `Prettify` utility type to enhance readability of hover overlays (type hints) for object types. This type simplifies complex object intersections, making them more legible in editor tooltips.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-node': minor
|
||||
---
|
||||
|
||||
feat(scaffolder): add first class citizen support for zod
|
||||
|
||||
This change introduces a new way to define template actions using Zod schemas for type safety. The existing `createTemplateAction` function is now renamed to `oldCreateTemplateAction` to maintain backwards compatibility. A new `createTemplateAction` function is introduced which acts as an overload, supporting both the old style (using JSON Schema or string schemas) via `oldCreateTemplateAction` and the new style (using Zod schemas) via `newCreateTemplateAction`. This new function, `newCreateTemplateAction`, provides direct support for Zod, simplifying action definition and enhancing type checking.
|
||||
@@ -79,6 +79,11 @@ export type Observer<T> = {
|
||||
complete?(): void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type Prettify<T extends Record<PropertyKey, unknown>> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
|
||||
// @public
|
||||
export type Subscription = {
|
||||
unsubscribe(): void;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A utility type that enhances the readability of hover overlays (type hints) for object types.
|
||||
*
|
||||
* @example Prettifying a basic object merge
|
||||
*
|
||||
* ### Basic object type
|
||||
* ```ts
|
||||
* type Example = { item1: string } & { item2: number }
|
||||
* // ?^ { item1: string } & { item2: number }
|
||||
* ```
|
||||
*
|
||||
* ### Usage
|
||||
* ```ts
|
||||
* type Example = Prettify<{ item1: string } & { item2: number }>
|
||||
* // ?^ { item1: string, item2: number }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type Prettify<T extends Record<PropertyKey, unknown>> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
@@ -14,24 +14,42 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ActionContext, TemplateAction } from './types';
|
||||
import type { JsonObject, Prettify } from '@backstage/types';
|
||||
import type { Schema } from 'jsonschema';
|
||||
import { z } from 'zod';
|
||||
import { Schema } from 'jsonschema';
|
||||
import zodToJsonSchema from 'zod-to-json-schema';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import type {
|
||||
InferActionType,
|
||||
NewActionContext,
|
||||
NewTemplateAction,
|
||||
OldActionContext,
|
||||
OldTemplateAction,
|
||||
TemplateExample,
|
||||
} from './types';
|
||||
|
||||
/** @public */
|
||||
export type TemplateExample = {
|
||||
description: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type TemplateActionOptions<
|
||||
TActionInput extends JsonObject = {},
|
||||
TActionOutput extends JsonObject = {},
|
||||
TInputSchema extends Schema | z.ZodType = {},
|
||||
TOutputSchema extends Schema | z.ZodType = {},
|
||||
/**
|
||||
* @deprecated migrate to {@link NewTemplateActionOptions}
|
||||
* @public
|
||||
*/
|
||||
export type OldTemplateActionOptions<
|
||||
TInputParams extends JsonObject = JsonObject,
|
||||
TOutputParams extends JsonObject = JsonObject,
|
||||
TInputSchema extends Schema | z.ZodType = Schema,
|
||||
TOutputSchema extends Schema | z.ZodType = Schema,
|
||||
TActionInput extends JsonObject = TInputSchema extends z.ZodType<
|
||||
any,
|
||||
any,
|
||||
infer IReturn
|
||||
>
|
||||
? IReturn
|
||||
: TInputParams,
|
||||
TActionOutput extends JsonObject = TOutputSchema extends z.ZodType<
|
||||
any,
|
||||
any,
|
||||
infer IReturn
|
||||
>
|
||||
? IReturn
|
||||
: TOutputParams,
|
||||
> = {
|
||||
id: string;
|
||||
description?: string;
|
||||
@@ -41,15 +59,69 @@ export type TemplateActionOptions<
|
||||
input?: TInputSchema;
|
||||
output?: TOutputSchema;
|
||||
};
|
||||
handler: (ctx: ActionContext<TActionInput, TActionOutput>) => Promise<void>;
|
||||
handler: (
|
||||
ctx: OldActionContext<TActionInput, TActionOutput>,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type NewTemplateActionOptions<
|
||||
TInputParams extends Record<
|
||||
PropertyKey,
|
||||
(zod: typeof z) => z.ZodType
|
||||
> = Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
TOutputParams extends Record<
|
||||
PropertyKey,
|
||||
(zod: typeof z) => z.ZodType
|
||||
> = Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
> = {
|
||||
id: string;
|
||||
description?: string;
|
||||
examples?: TemplateExample[];
|
||||
supportsDryRun?: boolean;
|
||||
schema: {
|
||||
input: TInputParams;
|
||||
output: TOutputParams;
|
||||
};
|
||||
handler: (
|
||||
ctx: NewActionContext<
|
||||
InferActionType<TInputParams>,
|
||||
InferActionType<TOutputParams>
|
||||
>,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type TemplateActionOptions<
|
||||
TInputParams extends Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
TOutputParams extends Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
> =
|
||||
| OldTemplateActionOptions
|
||||
| NewTemplateActionOptions<TInputParams, TOutputParams>;
|
||||
|
||||
function isZod(schema?: Schema | z.ZodType): schema is z.ZodType {
|
||||
return !!(schema && 'safeParseAsync' in schema);
|
||||
}
|
||||
|
||||
function transformZodRecordToObject(
|
||||
record: Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
): z.ZodObject<Record<PropertyKey, z.ZodType>> {
|
||||
return z.object(
|
||||
Object.fromEntries(Object.entries(record).map(([k, v]) => [k, v(z)])),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to create new template actions to get type safety.
|
||||
* Will convert zod schemas to json schemas for use throughout the system.
|
||||
* @deprecated migrate to {@link newCreateTemplateAction}
|
||||
* @public
|
||||
*/
|
||||
export const createTemplateAction = <
|
||||
export function oldCreateTemplateAction<
|
||||
TInputParams extends JsonObject = JsonObject,
|
||||
TOutputParams extends JsonObject = JsonObject,
|
||||
TInputSchema extends Schema | z.ZodType = {},
|
||||
@@ -69,29 +141,91 @@ export const createTemplateAction = <
|
||||
? IReturn
|
||||
: TOutputParams,
|
||||
>(
|
||||
action: TemplateActionOptions<
|
||||
TActionInput,
|
||||
TActionOutput,
|
||||
action: OldTemplateActionOptions<
|
||||
TInputParams,
|
||||
TOutputParams,
|
||||
TInputSchema,
|
||||
TOutputSchema
|
||||
TOutputSchema,
|
||||
TActionInput,
|
||||
TActionOutput
|
||||
>,
|
||||
): TemplateAction<TActionInput, TActionOutput> => {
|
||||
): OldTemplateAction<TActionInput, TActionOutput> {
|
||||
const inputSchema =
|
||||
action.schema?.input && 'safeParseAsync' in action.schema.input
|
||||
? zodToJsonSchema(action.schema.input)
|
||||
action.schema && action.schema.input && isZod(action.schema.input)
|
||||
? (zodToJsonSchema(action.schema.input) as Schema)
|
||||
: action.schema?.input;
|
||||
|
||||
const outputSchema =
|
||||
action.schema?.output && 'safeParseAsync' in action.schema.output
|
||||
? zodToJsonSchema(action.schema.output)
|
||||
action.schema && action.schema.output && isZod(action.schema.output)
|
||||
? (zodToJsonSchema(action.schema.output) as Schema)
|
||||
: action.schema?.output;
|
||||
|
||||
return {
|
||||
...action,
|
||||
schema: {
|
||||
...action.schema,
|
||||
input: inputSchema as TInputSchema,
|
||||
output: outputSchema as TOutputSchema,
|
||||
input: inputSchema,
|
||||
output: outputSchema,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to create new template actions to get type safety.
|
||||
* Will convert zod schemas to json schemas for use throughout the system.
|
||||
* @public
|
||||
*/
|
||||
export function newCreateTemplateAction<
|
||||
TInputParams extends Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
TOutputParams extends Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
>(
|
||||
action: NewTemplateActionOptions<TInputParams, TOutputParams>,
|
||||
): NewTemplateAction<
|
||||
InferActionType<TInputParams>,
|
||||
InferActionType<TOutputParams>
|
||||
> {
|
||||
const input = transformZodRecordToObject(action.schema.input);
|
||||
const output = transformZodRecordToObject(action.schema.output);
|
||||
|
||||
return {
|
||||
...action,
|
||||
schema: {
|
||||
...action.schema,
|
||||
input: zodToJsonSchema(input) as Schema,
|
||||
output: zodToJsonSchema(output) as Schema,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isOldAction(
|
||||
action: OldTemplateActionOptions | NewTemplateActionOptions,
|
||||
): action is OldTemplateActionOptions {
|
||||
return (
|
||||
isZod(action.schema?.input) ||
|
||||
typeof action.schema?.input === 'string' ||
|
||||
isZod(action.schema?.output) ||
|
||||
typeof action.schema?.output === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to create new template actions to get type safety.
|
||||
* Will convert zod schemas to json schemas for use throughout the system.
|
||||
* @public
|
||||
*/
|
||||
export function createTemplateAction<
|
||||
TInputParams extends JsonObject = JsonObject,
|
||||
TOutputParams extends JsonObject = JsonObject,
|
||||
TAction extends
|
||||
| OldTemplateActionOptions
|
||||
| NewTemplateActionOptions = OldTemplateActionOptions,
|
||||
TReturn = TAction extends OldTemplateActionOptions
|
||||
? Prettify<OldTemplateAction<TInputParams, TOutputParams>>
|
||||
: Prettify<NewTemplateAction>,
|
||||
>(action: TAction): TReturn {
|
||||
if (isOldAction(action)) {
|
||||
return oldCreateTemplateAction(action) as TReturn;
|
||||
}
|
||||
|
||||
return newCreateTemplateAction(action) as TReturn;
|
||||
}
|
||||
|
||||
@@ -16,21 +16,31 @@
|
||||
|
||||
export {
|
||||
createTemplateAction,
|
||||
type NewTemplateActionOptions,
|
||||
type OldTemplateActionOptions,
|
||||
type TemplateActionOptions,
|
||||
type TemplateExample,
|
||||
} from './createTemplateAction';
|
||||
export {
|
||||
executeShellCommand,
|
||||
type ExecuteShellCommandOptions,
|
||||
} from './executeShellCommand';
|
||||
export { fetchContents, fetchFile } from './fetch';
|
||||
export { type ActionContext, type TemplateAction } from './types';
|
||||
export {
|
||||
initRepoAndPush,
|
||||
commitAndPushRepo,
|
||||
commitAndPushBranch,
|
||||
addFiles,
|
||||
createBranch,
|
||||
cloneRepo,
|
||||
commitAndPushBranch,
|
||||
commitAndPushRepo,
|
||||
createBranch,
|
||||
initRepoAndPush,
|
||||
} from './gitHelpers';
|
||||
export { parseRepoUrl, getRepoSourceDirectory } from './util';
|
||||
export type {
|
||||
ActionContext,
|
||||
InferActionType,
|
||||
NewActionContext,
|
||||
NewTemplateAction,
|
||||
OldActionContext,
|
||||
OldTemplateAction,
|
||||
TemplateAction,
|
||||
TemplateExample,
|
||||
} from './types';
|
||||
export { getRepoSourceDirectory, parseRepoUrl } from './util';
|
||||
|
||||
@@ -14,21 +14,45 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import { Writable } from 'stream';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { TaskSecrets } from '../tasks';
|
||||
import { TemplateInfo } from '@backstage/plugin-scaffolder-common';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { Schema } from 'jsonschema';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import type {
|
||||
BackstageCredentials,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import type { UserEntity } from '@backstage/catalog-model';
|
||||
import type { TemplateInfo } from '@backstage/plugin-scaffolder-common';
|
||||
import type { JsonObject, JsonValue, Prettify } from '@backstage/types';
|
||||
import type { Schema } from 'jsonschema';
|
||||
import type { Writable } from 'stream';
|
||||
import type { Logger } from 'winston';
|
||||
import { z } from 'zod';
|
||||
import type { TaskSecrets } from '../tasks';
|
||||
|
||||
/**
|
||||
* ActionContext is passed into scaffolder actions.
|
||||
* @public
|
||||
*/
|
||||
export type ActionContext<
|
||||
TActionInput extends JsonObject,
|
||||
export type TemplateExample = {
|
||||
description: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type InferActionType<
|
||||
T extends Record<PropertyKey, (zod: typeof z) => z.ZodType>,
|
||||
> = Prettify<{
|
||||
[K in keyof T]: T[K] extends (
|
||||
zod: typeof z,
|
||||
) => z.ZodType<any, any, infer IReturn>
|
||||
? Extract<IReturn, JsonValue | undefined>
|
||||
: never;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* OldActionContext is passed into scaffolder actions.
|
||||
* @deprecated migrate to {@link NewActionContext}
|
||||
* @public
|
||||
*/
|
||||
export type OldActionContext<
|
||||
TActionInput extends JsonObject = JsonObject,
|
||||
TActionOutput extends JsonObject = JsonObject,
|
||||
> = {
|
||||
// TODO(blam): move this to LoggerService
|
||||
@@ -43,8 +67,10 @@ export type ActionContext<
|
||||
fn: () => Promise<T> | T;
|
||||
}): Promise<T>;
|
||||
output(
|
||||
name: keyof TActionOutput,
|
||||
value: TActionOutput[keyof TActionOutput],
|
||||
...params: {
|
||||
/* This maps the key to the value for type checking */
|
||||
[K in keyof TActionOutput]: [name: K, value: TActionOutput[K]];
|
||||
}[keyof TActionOutput]
|
||||
): void;
|
||||
|
||||
/**
|
||||
@@ -97,18 +123,129 @@ export type ActionContext<
|
||||
each?: JsonObject;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type TemplateAction<
|
||||
/**
|
||||
* NewActionContext is passed into scaffolder actions.
|
||||
* @public
|
||||
*/
|
||||
export type NewActionContext<
|
||||
TActionInput extends JsonObject = JsonObject,
|
||||
TActionOutput extends JsonObject = JsonObject,
|
||||
> = {
|
||||
logger: LoggerService;
|
||||
secrets?: TaskSecrets;
|
||||
workspacePath: string;
|
||||
input: TActionInput;
|
||||
checkpoint<U extends JsonValue>(
|
||||
key: string,
|
||||
fn: () => Promise<U>,
|
||||
): Promise<U>;
|
||||
output(
|
||||
...params: {
|
||||
/* This maps the key to the value for type checking */
|
||||
[K in keyof TActionOutput]: [name: K, value: TActionOutput[K]];
|
||||
}[keyof TActionOutput]
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Creates a temporary directory for use by the action, which is then cleaned up automatically.
|
||||
*/
|
||||
createTemporaryDirectory(): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get the credentials for the current request
|
||||
*/
|
||||
getInitiatorCredentials(): Promise<BackstageCredentials>;
|
||||
|
||||
templateInfo?: TemplateInfo;
|
||||
|
||||
/**
|
||||
* Whether this action invocation is a dry-run or not.
|
||||
* This will only ever be true if the actions as marked as supporting dry-runs.
|
||||
*/
|
||||
isDryRun?: boolean;
|
||||
|
||||
/**
|
||||
* The user which triggered the action.
|
||||
*/
|
||||
user?: {
|
||||
/**
|
||||
* The decorated entity from the Catalog
|
||||
*/
|
||||
entity?: UserEntity;
|
||||
/**
|
||||
* An entity ref for the author of the task
|
||||
*/
|
||||
ref?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
|
||||
/**
|
||||
* Optional value of each invocation
|
||||
*/
|
||||
each?: JsonObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ActionContext<
|
||||
TActionInput extends JsonObject = JsonObject,
|
||||
TActionOutput extends JsonObject = JsonObject,
|
||||
> =
|
||||
| OldActionContext<TActionInput, TActionOutput>
|
||||
| NewActionContext<TActionInput, TActionOutput>;
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link NewTemplateAction}
|
||||
* @public
|
||||
*/
|
||||
export type OldTemplateAction<
|
||||
TActionInput extends JsonObject = JsonObject,
|
||||
TActionOutput extends JsonObject = JsonObject,
|
||||
> = {
|
||||
id: string;
|
||||
description?: string;
|
||||
examples?: { description: string; example: string }[];
|
||||
examples?: TemplateExample[];
|
||||
supportsDryRun?: boolean;
|
||||
schema?: {
|
||||
input?: Schema;
|
||||
output?: Schema;
|
||||
};
|
||||
handler: (ctx: ActionContext<TActionInput, TActionOutput>) => Promise<void>;
|
||||
handler: (
|
||||
ctx: OldActionContext<TActionInput, TActionOutput>,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type NewTemplateAction<
|
||||
TActionInput extends JsonObject = JsonObject,
|
||||
TActionOutput extends JsonObject = JsonObject,
|
||||
> = {
|
||||
id: string;
|
||||
description?: string;
|
||||
examples?: TemplateExample[];
|
||||
supportsDryRun?: boolean;
|
||||
schema: {
|
||||
input: Schema;
|
||||
output: Schema;
|
||||
};
|
||||
handler: (
|
||||
ctx: NewActionContext<TActionInput, TActionOutput>,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type TemplateAction<
|
||||
TActionInput extends JsonObject = JsonObject,
|
||||
TActionOutput extends JsonObject = JsonObject,
|
||||
> =
|
||||
| OldTemplateAction<TActionInput, TActionOutput>
|
||||
| NewTemplateAction<TActionInput, TActionOutput>;
|
||||
|
||||
Reference in New Issue
Block a user