chore: refactor to use opaque type helper instead

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-11-19 12:01:44 +01:00
parent aac1e45bc5
commit f217918b14
12 changed files with 110 additions and 62 deletions
@@ -26,7 +26,7 @@ export const mockDecorator = createScaffolderFormDecorator({
deps: {
githubApi: githubAuthApiRef,
},
fn: async ({ setSecrets }, { githubApi }) => {
decorator: async ({ setSecrets }, { githubApi }) => {
const token = await githubApi.getAccessToken();
setSecrets(state => ({ ...state, GITHUB_TOKEN: token }));
},
@@ -23,6 +23,7 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-scaffolder-react": "workspace:^",
"zod": "^3.22.4"
},
@@ -0,0 +1,42 @@
/*
* 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.
*/
import { OpaqueType } from '@internal/opaque';
import { z } from 'zod';
import {
ScaffolderFormDecorator,
ScaffolderFormDecoratorContext,
} from '@backstage/plugin-scaffolder-react/alpha';
import { AnyApiRef } from '@backstage/frontend-plugin-api';
/** @alpha */
export const OpaqueFormDecorator = OpaqueType.create<{
public: ScaffolderFormDecorator;
versions: {
readonly version: 'v1';
readonly id: string;
readonly schema?: {
input?: {
[key in string]: (zImpl: typeof z) => z.ZodType;
};
};
readonly deps?: { [key in string]: AnyApiRef };
readonly decorator: (
ctx: ScaffolderFormDecoratorContext,
deps: { [depName in string]: AnyApiRef['T'] },
) => Promise<void>;
};
}>({ type: '@backstage/scaffolder/FormDecorator', versions: ['v1'] });
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { OpaqueFormField, type FormField } from './InternalFormField';
export { OpaqueFormDecorator } from './InternalFormDecorator';
@@ -106,7 +106,7 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
return;
}
await formDecorator.fn({
await formDecorator.decorator({
setSecrets,
setFormState: (
handler: (
@@ -116,7 +116,7 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
formState = { ...handler(formState) };
},
formState,
input: decorator.input,
input: decorator.input ?? {},
});
}),
);
@@ -20,7 +20,7 @@ describe('createScaffolderFormDecorator', () => {
const decorator = createScaffolderFormDecorator({
id: 'test',
deps: {},
fn: async () => {},
decorator: async () => {},
});
expect(decorator).toMatchInlineSnapshot(`
@@ -43,7 +43,7 @@ describe('createScaffolderFormDecorator', () => {
age: z => z.number(),
},
},
fn: async ctx => {
decorator: async ctx => {
const name: string = ctx.input.name;
// @ts-expect-error
@@ -14,11 +14,14 @@
* limitations under the License.
*/
import { AnyApiRef } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { JsonObject, JsonValue } from '@backstage/types';
import { OpaqueFormDecorator } from '@internal/scaffolder';
import { z } from 'zod';
/** @alpha */
export type ScaffolderFormDecoratorContext<TInput> = {
export type ScaffolderFormDecoratorContext<
TInput extends JsonObject = JsonObject,
> = {
input: TInput;
formState: Record<string, JsonValue>;
@@ -31,53 +34,51 @@ export type ScaffolderFormDecoratorContext<TInput> = {
};
/** @alpha */
export type ScaffolderFormDecorator<
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType },
TDeps extends { [key in string]: AnyApiRef },
> = {
version: 'v1';
id: string;
schema?: {
input?: TInputSchema;
};
deps?: TDeps;
fn: (
ctx: ScaffolderFormDecoratorContext<{
[key in keyof TInputSchema]: z.infer<ReturnType<TInputSchema[key]>>;
}>,
deps: TDeps extends { [key in string]: AnyApiRef }
? { [key in keyof TDeps]: TDeps[key]['T'] }
: never,
) => Promise<void>;
export type ScaffolderFormDecorator<TInput extends JsonObject = JsonObject> = {
readonly $$type: '@backstage/scaffolder/FormDecorator';
readonly id: string;
readonly TInput: TInput;
};
/** @alpha */
export type AnyScaffolderFormDecorator = ScaffolderFormDecorator<any, any>;
/**
* Method for creating decorators which can be used to collect
* secrets from the user before submitting to the backend.
* @alpha
*/
export function createScaffolderFormDecorator<
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType },
TDeps extends { [key in string]: AnyApiRef },
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } = {
[key in string]: (zImpl: typeof z) => z.ZodType;
},
TDeps extends { [key in string]: AnyApiRef } = { [key in string]: AnyApiRef },
TInput extends JsonObject = {
[key in keyof TInputSchema]: z.infer<ReturnType<TInputSchema[key]>>;
},
>(options: {
id: string;
schema?: {
input?: TInputSchema;
};
deps?: TDeps;
fn: (
ctx: ScaffolderFormDecoratorContext<{
[key in keyof TInputSchema]: z.infer<ReturnType<TInputSchema[key]>>;
}>,
decorator: (
ctx: ScaffolderFormDecoratorContext<TInput>,
deps: TDeps extends { [key in string]: AnyApiRef }
? { [key in keyof TDeps]: TDeps[key]['T'] }
: never,
) => Promise<void>;
}): ScaffolderFormDecorator<TInputSchema, TDeps> {
return {
}): ScaffolderFormDecorator<TInput> {
return OpaqueFormDecorator.createInstance('v1', {
...options,
version: 'v1',
};
TInput: null as unknown as TInput,
} as {
id: string;
schema?: {
input?: TInputSchema;
};
TInput: TInput;
deps?: TDeps;
decorator: (
ctx: ScaffolderFormDecoratorContext,
deps: { [key in string]: AnyApiRef['T'] },
) => Promise<void>;
});
}
@@ -15,7 +15,7 @@
*/
import { ScaffolderFormDecoratorsApi } from './types';
import { AnyScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
/** @alpha */
export class DefaultScaffolderFormDecoratorsApi
@@ -23,17 +23,17 @@ export class DefaultScaffolderFormDecoratorsApi
{
private constructor(
private readonly options: {
decorators: Array<AnyScaffolderFormDecorator>;
decorators: Array<ScaffolderFormDecorator>;
},
) {}
static create(options?: { decorators: AnyScaffolderFormDecorator[] }) {
static create(options?: { decorators: ScaffolderFormDecorator[] }) {
return new DefaultScaffolderFormDecoratorsApi(
options ?? { decorators: [] },
);
}
async getFormDecorators(): Promise<AnyScaffolderFormDecorator[]> {
async getFormDecorators(): Promise<ScaffolderFormDecorator[]> {
return this.options.decorators;
}
}
+2 -2
View File
@@ -16,7 +16,7 @@
import {
FormFieldExtensionData,
AnyScaffolderFormDecorator,
ScaffolderFormDecorator,
} from '@backstage/plugin-scaffolder-react/alpha';
export interface ScaffolderFormFieldsApi {
@@ -24,5 +24,5 @@ export interface ScaffolderFormFieldsApi {
}
export interface ScaffolderFormDecoratorsApi {
getFormDecorators(): Promise<AnyScaffolderFormDecorator[]>;
getFormDecorators(): Promise<ScaffolderFormDecorator[]>;
}
@@ -38,7 +38,7 @@ describe('useFormDecorators', () => {
test: z => z.string(),
},
},
async fn({ input: { test } }, { mockApiRef: mock }) {
async decorator({ input: { test } }, { mockApiRef: mock }) {
mock.test(test);
},
});
@@ -70,7 +70,7 @@ describe('useFormDecorators', () => {
const testDecorator = result.get('test')!;
expect(testDecorator).toBeDefined();
await testDecorator.fn({
await testDecorator.decorator({
formState: {},
setFormState: () => {},
setSecrets: () => {},
@@ -13,19 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AnyApiRef, useApi, useApiHolder } from '@backstage/core-plugin-api';
import { errorApiRef, useApi, useApiHolder } from '@backstage/core-plugin-api';
import { formDecoratorsApiRef } from '../api/ref';
import useAsync from 'react-use/esm/useAsync';
import { useMemo } from 'react';
import { ScaffolderFormDecoratorContext } from '@backstage/plugin-scaffolder-react/alpha';
import { OpaqueFormDecorator } from '@internal/scaffolder';
/** @internal */
type BoundFieldDecorator = {
fn: (ctx: ScaffolderFormDecoratorContext<any>) => Promise<void>;
decorator: (ctx: ScaffolderFormDecoratorContext) => Promise<void>;
};
export const useFormDecorators = () => {
const formDecoratorsApi = useApi(formDecoratorsApiRef);
const errorApi = useApi(errorApiRef);
const { value: decorators } = useAsync(
() => formDecoratorsApi.getFormDecorators(),
[],
@@ -37,27 +39,27 @@ export const useFormDecorators = () => {
for (const decorator of decorators ?? []) {
try {
const resolvedDeps = Object.entries(decorator.deps ?? {}).map(
([key, value]) => {
const api = apiHolder.get(value);
if (!api) {
throw new Error(
`Failed to resolve apiRef ${value.id} for form decorator ${decorator.id} it will be disabled`,
);
}
return [key, api];
},
);
const { decorator: decoratorFn, deps } =
OpaqueFormDecorator.toInternal(decorator);
const resolvedDeps = Object.entries(deps ?? {}).map(([key, value]) => {
const api = apiHolder.get(value);
if (!api) {
throw new Error(
`Failed to resolve apiRef ${value.id} for form decorator ${decorator.id} it will be disabled`,
);
}
return [key, api];
});
decoratorsMap.set(decorator.id, {
fn: ctx => decorator.fn(ctx, Object.fromEntries(resolvedDeps)),
decorator: ctx => decoratorFn(ctx, Object.fromEntries(resolvedDeps)),
});
} catch (ex) {
// eslint-disable-next-line no-console
console.error(ex);
errorApi.post(ex);
return undefined;
}
}
return decoratorsMap;
}, [apiHolder, decorators]);
}, [apiHolder, decorators, errorApi]);
};
+1
View File
@@ -10339,6 +10339,7 @@ __metadata:
resolution: "@internal/scaffolder@workspace:packages/scaffolder-internal"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-scaffolder-react": "workspace:^"
zod: ^3.22.4
languageName: unknown