From 28377dc89f1f33baf8051d0fdd7b0091e0ac97da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Sep 2022 18:49:05 +0200 Subject: [PATCH] backend-plugin-api: fix type inference of interface options Signed-off-by: Patrik Oldsberg --- .changeset/itchy-schools-run.md | 5 + packages/backend-plugin-api/api-report.md | 25 +--- .../src/services/system/types.test.ts | 137 ++++++++++++++++++ .../src/services/system/types.ts | 2 +- .../src/wiring/factories.test.ts | 66 +++++++++ .../src/wiring/factories.ts | 11 +- 6 files changed, 222 insertions(+), 24 deletions(-) create mode 100644 .changeset/itchy-schools-run.md diff --git a/.changeset/itchy-schools-run.md b/.changeset/itchy-schools-run.md new file mode 100644 index 0000000000..6e46cabb15 --- /dev/null +++ b/.changeset/itchy-schools-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Allow interfaces to be used for inferred option types. diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 3344576db8..2d4e431502 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -73,11 +73,7 @@ export const configServiceRef: ServiceRef; // @public (undocumented) export function createBackendModule< - TOptions extends - | { - [name: string]: unknown; - } - | undefined = undefined, + TOptions extends object | undefined = undefined, >( config: BackendModuleConfig, ): undefined extends TOptions @@ -86,14 +82,11 @@ export function createBackendModule< // @public (undocumented) export function createBackendPlugin< - TOptions extends - | { - [name: string]: unknown; - } - | undefined = undefined, ->( - config: BackendPluginConfig, -): undefined extends TOptions + TOptions extends object | undefined = undefined, +>(config: { + id: string; + register(reg: BackendRegistrationPoints, options: TOptions): void; +}): undefined extends TOptions ? (options?: TOptions) => BackendFeature : (options: TOptions) => BackendFeature; @@ -110,11 +103,7 @@ export function createServiceFactory< TDeps extends { [name in string]: ServiceRef; }, - TOpts extends - | { - [name in string]: unknown; - } - | undefined = undefined, + TOpts extends object | undefined = undefined, >(config: { service: ServiceRef; deps: TDeps; diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts index 29512d3a58..0e02f38d3f 100644 --- a/packages/backend-plugin-api/src/services/system/types.test.ts +++ b/packages/backend-plugin-api/src/services/system/types.test.ts @@ -58,6 +58,8 @@ describe('createServiceFactory', () => { metaFactory({}); metaFactory({ x: 1 }); // @ts-expect-error + metaFactory({ x: 1, y: 2 }); + // @ts-expect-error metaFactory(null); metaFactory(undefined); metaFactory(); @@ -80,10 +82,145 @@ describe('createServiceFactory', () => { metaFactory({}); metaFactory({ x: 1 }); // @ts-expect-error + metaFactory({ x: 1, y: 2 }); + // @ts-expect-error metaFactory(null); // @ts-expect-error metaFactory(undefined); // @ts-expect-error metaFactory(); }); + + it('should create a meta factory with optional options as interface', () => { + interface TestOptions { + x: number; + } + const ref = createServiceRef({ id: 'x' }); + const metaFactory = createServiceFactory({ + service: ref, + deps: {}, + async factory(_deps, _opts?: TestOptions) { + return async () => 'x'; + }, + }); + expect(metaFactory).toEqual(expect.any(Function)); + + // @ts-expect-error + metaFactory('string'); + // @ts-expect-error + metaFactory({}); + metaFactory({ x: 1 }); + // @ts-expect-error + metaFactory({ x: 1, y: 2 }); + // @ts-expect-error + metaFactory(null); + metaFactory(undefined); + metaFactory(); + }); + + it('should create a meta factory with required options as interface', () => { + interface TestOptions { + x: number; + } + const ref = createServiceRef({ id: 'x' }); + const metaFactory = createServiceFactory({ + service: ref, + deps: {}, + async factory(_deps, _opts: TestOptions) { + return async () => 'x'; + }, + }); + expect(metaFactory).toEqual(expect.any(Function)); + + // @ts-expect-error + metaFactory('string'); + // @ts-expect-error + metaFactory({}); + metaFactory({ x: 1 }); + // @ts-expect-error + metaFactory({ x: 1, y: 2 }); + // @ts-expect-error + metaFactory(null); + // @ts-expect-error + metaFactory(undefined); + // @ts-expect-error + metaFactory(); + }); + + it('should only allow objects as options', () => { + const ref = createServiceRef({ id: 'x' }); + const metaFactory = createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: string) { + return async () => 'x'; + }, + }); + expect(metaFactory).toEqual(expect.any(Function)); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: number) { + return async () => 'x'; + }, + }); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: symbol) { + return async () => 'x'; + }, + }); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: bigint) { + return async () => 'x'; + }, + }); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: 'string') { + return async () => 'x'; + }, + }); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: Array) { + return async () => 'x'; + }, + }); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: Map) { + return async () => 'x'; + }, + }); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: Set) { + return async () => 'x'; + }, + }); + createServiceFactory({ + service: ref, + deps: {}, + // @ts-expect-error + async factory(_deps, _opts: null) { + return async () => 'x'; + }, + }); + }); }); diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index d20fd6fd8f..2459dea918 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -133,7 +133,7 @@ export function createServiceFactory< TScope extends 'root' | 'plugin', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, - TOpts extends { [name in string]: unknown } | undefined = undefined, + TOpts extends object | undefined = undefined, >(config: { service: ServiceRef; deps: TDeps; diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/factories.test.ts index 110d7b4f3f..8dbd5c5479 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/factories.test.ts @@ -68,6 +68,38 @@ describe('createBackendPlugin', () => { // @ts-expect-error expect(plugin({})).toBeDefined(); }); + + it('should create a BackendPlugin with options as interface', () => { + interface TestOptions { + a: string; + } + const plugin = createBackendPlugin({ + id: 'x', + register(_reg, _options: TestOptions) {}, + }); + expect(plugin).toBeDefined(); + expect(plugin({ a: 'a' })).toBeDefined(); + expect(plugin({ a: 'a' }).id).toBe('x'); + // @ts-expect-error + expect(plugin()).toBeDefined(); + // @ts-expect-error + expect(plugin({ b: 'b' })).toBeDefined(); + }); + + it('should create plugins with optional options as interface', () => { + interface TestOptions { + a: string; + } + const plugin = createBackendPlugin({ + id: 'x', + register(_reg, _options?: TestOptions) {}, + }); + expect(plugin).toBeDefined(); + expect(plugin({ a: 'a' })).toBeDefined(); + expect(plugin()).toBeDefined(); + // @ts-expect-error + expect(plugin({ b: 'b' })).toBeDefined(); + }); }); describe('createBackendModule', () => { @@ -111,4 +143,38 @@ describe('createBackendModule', () => { // @ts-expect-error expect(mod({})).toBeDefined(); }); + + it('should create a BackendModule as interface', () => { + interface TestOptions { + a: string; + } + const mod = createBackendModule({ + pluginId: 'x', + moduleId: 'y', + register(_reg, _options: TestOptions) {}, + }); + expect(mod).toBeDefined(); + expect(mod({ a: 'a' })).toBeDefined(); + expect(mod({ a: 'a' }).id).toBe('x.y'); + // @ts-expect-error + expect(mod()).toBeDefined(); + // @ts-expect-error + expect(mod({ b: 'b' })).toBeDefined(); + }); + + it('should create modules with optional options as interface', () => { + interface TestOptions { + a: string; + } + const mod = createBackendModule({ + pluginId: 'x', + moduleId: 'y', + register(_reg, _options?: TestOptions) {}, + }); + expect(mod).toBeDefined(); + expect(mod({ a: 'a' })).toBeDefined(); + expect(mod()).toBeDefined(); + // @ts-expect-error + expect(mod({ b: 'b' })).toBeDefined(); + }); }); diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts index 491a18c9db..35f1ad1b7c 100644 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ b/packages/backend-plugin-api/src/wiring/factories.ts @@ -44,10 +44,11 @@ export interface BackendPluginConfig { /** @public */ export function createBackendPlugin< - TOptions extends { [name: string]: unknown } | undefined = undefined, ->( - config: BackendPluginConfig, -): undefined extends TOptions + TOptions extends object | undefined = undefined, +>(config: { + id: string; + register(reg: BackendRegistrationPoints, options: TOptions): void; +}): undefined extends TOptions ? (options?: TOptions) => BackendFeature : (options: TOptions) => BackendFeature { return (options?: TOptions) => ({ @@ -70,7 +71,7 @@ export interface BackendModuleConfig { /** @public */ export function createBackendModule< - TOptions extends { [name: string]: unknown } | undefined = undefined, + TOptions extends object | undefined = undefined, >( config: BackendModuleConfig, ): undefined extends TOptions