backend-plugin-api: fix for plugins and modules depending on multion services

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-08-07 12:48:09 +02:00
parent 61e4df970c
commit ddde5fe772
9 changed files with 187 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Fixed a type issue where plugin and modules depending on multiton services would not receive the correct type.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Internal type refactor.
@@ -245,6 +245,7 @@ Monorepo
monorepos
msgraph
msw
multiton
mutex
mutexes
mysql
@@ -342,6 +342,62 @@ describe('BackendInitializer', () => {
await init.start();
});
it('should allow plugins and modules depend on multiton services', async () => {
expect.assertions(2);
const multiServiceRef = createServiceRef<string>({
id: 'a',
multiton: true,
});
const init = new BackendInitializer(baseFactories);
init.add(
createServiceFactory({
service: multiServiceRef,
deps: {},
factory: () => 'x',
}),
);
init.add(
createServiceFactory({
service: multiServiceRef,
deps: {},
factory: () => 'y',
}),
);
init.add(
createBackendPlugin({
pluginId: 'test',
register(reg) {
reg.registerInit({
deps: { multi: multiServiceRef },
async init({ multi }) {
expect(multi).toEqual(['x', 'y']);
},
});
},
}),
);
init.add(
createBackendModule({
pluginId: 'test',
moduleId: 'test',
register(reg) {
reg.registerInit({
deps: { multi: multiServiceRef },
async init({ multi }) {
expect(multi).toEqual(['x', 'y']);
},
});
},
}),
);
await init.start();
});
it('should forward errors when plugins fail to start', async () => {
const init = new BackendInitializer([]);
init.add(
@@ -18,6 +18,7 @@ import {
AuthService,
coreServices,
createBackendPlugin,
HttpRouterService,
ServiceRef,
} from '@backstage/backend-plugin-api';
import { RequestHandler } from 'express';
@@ -107,7 +108,10 @@ export function makeLegacyPlugin<
return [key, transform(dep)];
}
if (key === 'tokenManager') {
return [key, wrapTokenManager(dep as TokenManager, _auth)];
return [
key,
wrapTokenManager(dep as TokenManager, _auth as AuthService),
];
}
return [key, dep];
}),
@@ -115,7 +119,7 @@ export function makeLegacyPlugin<
const router = await createRouter(
pluginEnv as TransformedEnv<TEnv, TEnvTransforms>,
);
_router.use(router);
(_router as HttpRouterService).use(router);
},
});
},
+8 -12
View File
@@ -82,14 +82,12 @@ export interface BackendModuleRegistrationPoints {
): void;
// (undocumented)
registerInit<
Deps extends {
[name in string]: unknown;
TDeps extends {
[name in string]: ServiceRef<unknown> | ExtensionPoint<unknown>;
},
>(options: {
deps: {
[name in keyof Deps]: ServiceRef<Deps[name]> | ExtensionPoint<Deps[name]>;
};
init(deps: Deps): Promise<void>;
deps: TDeps;
init(deps: DepsToInstances<TDeps>): Promise<void>;
}): void;
}
@@ -105,14 +103,12 @@ export interface BackendPluginRegistrationPoints {
): void;
// (undocumented)
registerInit<
Deps extends {
[name in string]: unknown;
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
>(options: {
deps: {
[name in keyof Deps]: ServiceRef<Deps[name]>;
};
init(deps: Deps): Promise<void>;
deps: TDeps;
init(deps: DepsToInstances<TDeps>): Promise<void>;
}): void;
}
@@ -14,7 +14,9 @@
* limitations under the License.
*/
import { createServiceRef } from '../services';
import { createBackendModule } from './createBackendModule';
import { createExtensionPoint } from './createExtensionPoint';
import { InternalBackendRegistrations } from './types';
describe('createBackendModule', () => {
@@ -54,4 +56,35 @@ describe('createBackendModule', () => {
// @ts-expect-error
expect(module({ a: 'a' })).toBeDefined();
});
it('should be able to depend on all types of dependencies', () => {
const extensionPoint = createExtensionPoint<string>({ id: 'point' });
const singleServiceRef = createServiceRef<string>({ id: 'single' });
const multiServiceRef = createServiceRef<string>({
id: 'multi',
multiton: true,
});
const plugin = createBackendModule({
pluginId: 'x',
moduleId: 'y',
register(r) {
r.registerInit({
deps: {
point: extensionPoint,
single: singleServiceRef,
multi: multiServiceRef,
},
async init({ point, single, multi }) {
const a: string = point;
const b: string = single;
const c: string[] = multi;
expect([a, b, c]).toBe('unused');
},
});
},
});
expect(plugin.$$type).toEqual('@backstage/BackendFeature');
});
});
@@ -14,7 +14,9 @@
* limitations under the License.
*/
import { createServiceRef } from '../services';
import { createBackendPlugin } from './createBackendPlugin';
import { createExtensionPoint } from './createExtensionPoint';
import { InternalBackendRegistrations } from './types';
describe('createBackendPlugin', () => {
@@ -52,4 +54,50 @@ describe('createBackendPlugin', () => {
// @ts-expect-error
expect(plugin({ a: 'a' })).toBeDefined();
});
it('should be able to depend on all compatible dependencies', () => {
const singleServiceRef = createServiceRef<string>({ id: 'single' });
const multiServiceRef = createServiceRef<string>({
id: 'multi',
multiton: true,
});
const plugin = createBackendPlugin({
pluginId: 'x',
register(r) {
r.registerInit({
deps: {
single: singleServiceRef,
multi: multiServiceRef,
},
async init({ single, multi }) {
const a: string = single;
const b: string[] = multi;
expect([a, b]).toBe('unused');
},
});
},
});
expect(plugin.$$type).toEqual('@backstage/BackendFeature');
});
it('should not be able to depend on extension points', () => {
const extensionPoint = createExtensionPoint<string>({ id: 'point' });
const plugin = createBackendPlugin({
pluginId: 'x',
register(r) {
r.registerInit({
deps: {
// @ts-expect-error
point: extensionPoint,
},
async init() {},
});
},
});
expect(plugin.$$type).toEqual('@backstage/BackendFeature');
});
});
+25 -10
View File
@@ -36,6 +36,17 @@ export type ExtensionPoint<T> = {
$$type: '@backstage/ExtensionPoint';
};
/** @ignore */
type DepsToInstances<
TDeps extends {
[key in string]: ServiceRef<unknown> | ExtensionPoint<unknown>;
},
> = {
[key in keyof TDeps]: TDeps[key] extends ServiceRef<unknown, any, 'multiton'>
? Array<TDeps[key]['T']>
: TDeps[key]['T'];
};
/**
* The callbacks passed to the `register` method of a backend plugin.
*
@@ -46,11 +57,13 @@ export interface BackendPluginRegistrationPoints {
ref: ExtensionPoint<TExtensionPoint>,
impl: TExtensionPoint,
): void;
registerInit<Deps extends { [name in string]: unknown }>(options: {
deps: {
[name in keyof Deps]: ServiceRef<Deps[name]>;
};
init(deps: Deps): Promise<void>;
registerInit<
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
>(options: {
deps: TDeps;
init(deps: DepsToInstances<TDeps>): Promise<void>;
}): void;
}
@@ -64,11 +77,13 @@ export interface BackendModuleRegistrationPoints {
ref: ExtensionPoint<TExtensionPoint>,
impl: TExtensionPoint,
): void;
registerInit<Deps extends { [name in string]: unknown }>(options: {
deps: {
[name in keyof Deps]: ServiceRef<Deps[name]> | ExtensionPoint<Deps[name]>;
};
init(deps: Deps): Promise<void>;
registerInit<
TDeps extends {
[name in string]: ServiceRef<unknown> | ExtensionPoint<unknown>;
},
>(options: {
deps: TDeps;
init(deps: DepsToInstances<TDeps>): Promise<void>;
}): void;
}