frontend-internal: switch opaqueType.createInstance to decorate + select version

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-09-11 14:04:39 +02:00
parent 4cfacd936b
commit bb0ea523b9
3 changed files with 102 additions and 150 deletions
@@ -33,42 +33,20 @@ describe('OpaqueType', () => {
versions: ['v1'],
});
OpaqueMyType.createInstance({
// @ts-expect-error - wrong type
$$type: 'wrong-type',
version: 'v1',
foo: 'bar',
});
OpaqueMyType.createInstance({
$$type: 'my-type',
// @ts-expect-error - unsupported version
version: 'v2',
foo: 'bar',
});
// @ts-expect-error - missing version
OpaqueMyType.createInstance({
$$type: 'my-type',
// @ts-expect-error - unsupported version
OpaqueMyType.createInstance('v2', {
foo: 'bar',
});
// @ts-expect-error - missing internal field
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
});
OpaqueMyType.createInstance('v1', {});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
OpaqueMyType.createInstance('v1', {
// @ts-expect-error - invalid internal field
foo: 3,
});
const myInstance = OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
const myInstance = OpaqueMyType.createInstance('v1', {
foo: 'bar',
});
@@ -170,62 +148,34 @@ describe('OpaqueType', () => {
versions: ['v1', 'v2', 'v3'],
});
OpaqueMyType.createInstance({
// @ts-expect-error - wrong type
$$type: 'wrong-type',
version: 'v1',
foo: 'bar',
});
OpaqueMyType.createInstance({
$$type: 'my-type',
// @ts-expect-error - unsupported version
version: 'v0',
foo: 'bar',
});
// @ts-expect-error - missing version
OpaqueMyType.createInstance({
$$type: 'my-type',
// @ts-expect-error - unsupported version
OpaqueMyType.createInstance('v0', {
foo: 'bar',
});
// @ts-expect-error - missing internal field
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
});
OpaqueMyType.createInstance('v1', {});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
OpaqueMyType.createInstance('v1', {
// @ts-expect-error - invalid internal field
foo: 3,
});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v2',
OpaqueMyType.createInstance('v2', {
// @ts-expect-error - version mismatch
foo: 'bar',
});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
OpaqueMyType.createInstance('v1', {
// @ts-expect-error - version mismatch
bar: 'foo',
});
const myInstanceV1 = OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
const myInstanceV1 = OpaqueMyType.createInstance('v1', {
foo: 'bar',
});
const myInstanceV2 = OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v2',
const myInstanceV2 = OpaqueMyType.createInstance('v2', {
bar: 'foo',
});
@@ -293,35 +243,20 @@ describe('OpaqueType', () => {
versions: [undefined],
});
OpaqueMyType.createInstance({
// @ts-expect-error - wrong type
$$type: 'wrong-type',
foo: 'bar',
});
OpaqueMyType.createInstance({
$$type: 'my-type',
// @ts-expect-error - unsupported version
version: 'v1',
// @ts-expect-error - unsupported version
OpaqueMyType.createInstance('v1', {
foo: 'bar',
});
// @ts-expect-error - missing internal field
OpaqueMyType.createInstance({
$$type: 'my-type',
version: undefined,
});
OpaqueMyType.createInstance(undefined, {});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: undefined,
OpaqueMyType.createInstance(undefined, {
// @ts-expect-error - invalid internal field
foo: 3,
});
const myInstance = OpaqueMyType.createInstance({
$$type: 'my-type',
version: undefined,
const myInstance = OpaqueMyType.createInstance(undefined, {
foo: 'bar',
});
@@ -370,62 +305,34 @@ describe('OpaqueType', () => {
versions: [undefined, 'v1'],
});
OpaqueMyType.createInstance({
// @ts-expect-error - wrong type
$$type: 'wrong-type',
version: 'v1',
foo: 'bar',
});
OpaqueMyType.createInstance({
$$type: 'my-type',
// @ts-expect-error - unsupported version
version: 'v3',
foo: 'bar',
});
// @ts-expect-error - missing version
OpaqueMyType.createInstance({
$$type: 'my-type',
// @ts-expect-error - unsupported version
OpaqueMyType.createInstance('v0', {
foo: 'bar',
});
// @ts-expect-error - missing internal field
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
});
OpaqueMyType.createInstance('v1', {});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
OpaqueMyType.createInstance('v1', {
// @ts-expect-error - invalid internal field
foo: 3,
});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: undefined,
OpaqueMyType.createInstance(undefined, {
// @ts-expect-error - version mismatch
foo: 'bar',
});
OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
OpaqueMyType.createInstance('v1', {
// @ts-expect-error - version mismatch
bar: 'foo',
});
const myInstanceV1 = OpaqueMyType.createInstance({
$$type: 'my-type',
version: 'v1',
const myInstanceV1 = OpaqueMyType.createInstance('v1', {
foo: 'bar',
});
const myInstanceV2 = OpaqueMyType.createInstance({
$$type: 'my-type',
version: undefined,
const myInstanceV2 = OpaqueMyType.createInstance(undefined, {
bar: 'foo',
});
@@ -472,4 +379,38 @@ describe('OpaqueType', () => {
`"Invalid opaque type instance, got version 'v3', expected undefined or 'v1'"`,
);
});
it('should create an empty opaque type with no versions', () => {
type MyType = {
$$type: 'my-type';
};
const OpaqueMyType = OpaqueType.create<{
public: MyType;
versions: {
version: undefined;
};
}>({
type: 'my-type',
versions: [undefined],
});
// @ts-expect-error - unsupported version
OpaqueMyType.createInstance('v0', {
foo: 'bar',
});
const myInstance = OpaqueMyType.createInstance(undefined, {});
expect(myInstance.$$type).toBe('my-type');
expect(OpaqueMyType.isType(myInstance)).toBe(true);
expect(OpaqueMyType.isType('hello')).toBe(false);
const myInternal = OpaqueMyType.toInternal(myInstance);
expect(myInternal).toBe(myInstance);
// All fields accessible
expect(myInternal.$$type).toBe('my-type');
expect(myInternal.version).toBe(undefined);
});
});
@@ -116,14 +116,27 @@ export class OpaqueType<
}
/**
* Creates an instance of the opaque type, returning the public public type.
* Creates an instance of the opaque type, returning the public type.
*
* By providing a type argument you can narrow the return to specific type parameters.
* @param version The version of the instance to create
* @param value The remaining public and internal properties of the instance
* @returns An instance of the opaque type
*/
createInstance<TBase extends T['public'] = T['public']>(
value: T['public'] & T['versions'] & Object, // & Object to allow for object properties too, e.g. toString()
): TBase {
return value as unknown as TBase;
createInstance<TVersion extends T['versions']['version']>(
version: TVersion,
props: Omit<T['public'], '$$type'> &
(T['versions'] extends infer UVersion
? UVersion extends { version: TVersion }
? Omit<UVersion, 'version'>
: never
: never) &
Object, // & Object to allow for object properties too, e.g. toString()
): T['public'] {
return {
...(props as object),
$$type: this.#type,
...(version && { version }),
} as T['public'];
}
#isThisInternalType(value: unknown): value is T['public'] & T['versions'] {
@@ -366,6 +366,26 @@ export function createExtension<
namespace: string | undefined extends TNamespace ? undefined : TNamespace;
name: string | undefined extends TName ? undefined : TName;
}> {
type T = {
config: string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>;
output: UOutput;
inputs: TInputs;
kind: string | undefined extends TKind ? undefined : TKind;
namespace: string | undefined extends TNamespace ? undefined : TNamespace;
name: string | undefined extends TName ? undefined : TName;
};
const schemaDeclaration = options.config?.schema;
const configSchema =
schemaDeclaration &&
@@ -377,30 +397,8 @@ export function createExtension<
),
);
return OpaqueExtensionDefinition.createInstance({
$$type: '@backstage/ExtensionDefinition',
version: 'v2',
T: undefined as unknown as {
config: string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>;
output: UOutput;
inputs: TInputs;
kind: string | undefined extends TKind ? undefined : TKind;
namespace: string | undefined extends TNamespace ? undefined : TNamespace;
name: string | undefined extends TName ? undefined : TName;
},
return OpaqueExtensionDefinition.createInstance('v2', {
T: undefined as unknown as T,
kind: options.kind,
namespace: options.namespace,
name: options.name,
@@ -512,5 +510,5 @@ export function createExtension<
},
}) as ExtensionDefinition<any>;
},
});
}) as ExtensionDefinition<T>;
}