Merge pull request #13618 from backstage/mob/refactories
backstage-plugin-api: introduce service scope and rework service factories
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Service are now scoped to either `'plugin'` or `'root'` scope. Service factories have been updated to provide dependency instances directly rather than factory functions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-node': patch
|
||||
---
|
||||
|
||||
Updated usage of experimental backend service APIs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Updated service implementations and backend wiring to support scoped service.
|
||||
@@ -18,6 +18,7 @@ import { CacheManager } from '@backstage/backend-common';
|
||||
import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
pluginMetadataServiceRef,
|
||||
cacheServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
@@ -25,13 +26,13 @@ import {
|
||||
export const cacheFactory = createServiceFactory({
|
||||
service: cacheServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
return async (pluginId: string) => {
|
||||
return cacheManager.forPlugin(pluginId);
|
||||
return async ({ plugin }) => {
|
||||
return cacheManager.forPlugin(plugin.getId());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,23 +19,20 @@ import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
loggerToWinstonLogger,
|
||||
loggerServiceRef,
|
||||
rootLoggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export const configFactory = createServiceFactory({
|
||||
service: configServiceRef,
|
||||
deps: {
|
||||
loggerFactory: loggerServiceRef,
|
||||
logger: rootLoggerServiceRef,
|
||||
},
|
||||
factory: async ({ loggerFactory }) => {
|
||||
const logger = await loggerFactory('root');
|
||||
async factory({ logger }) {
|
||||
const config = await loadBackendConfig({
|
||||
argv: process.argv,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
});
|
||||
return async () => {
|
||||
return config;
|
||||
};
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,19 +19,20 @@ import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
databaseServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export const databaseFactory = createServiceFactory({
|
||||
service: databaseServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
return async (pluginId: string) => {
|
||||
return databaseManager.forPlugin(pluginId);
|
||||
return async ({ plugin }) => {
|
||||
return databaseManager.forPlugin(plugin.getId());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,10 +25,9 @@ import {
|
||||
export const discoveryFactory = createServiceFactory({
|
||||
service: discoveryServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
return async () => {
|
||||
return discovery;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createServiceFactory,
|
||||
httpRouterServiceRef,
|
||||
configServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import Router from 'express-promise-router';
|
||||
import { Handler } from 'express';
|
||||
@@ -27,18 +28,20 @@ import { createServiceBuilder } from '@backstage/backend-common';
|
||||
export const httpRouterFactory = createServiceFactory({
|
||||
service: httpRouterServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
async factory({ config }) {
|
||||
const rootRouter = Router();
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(await configFactory('root'))
|
||||
.loadConfig(config)
|
||||
.addRouter('', rootRouter);
|
||||
|
||||
await service.start();
|
||||
|
||||
return async (pluginId?: string) => {
|
||||
return async ({ plugin }) => {
|
||||
const pluginId = plugin.getId();
|
||||
const path = pluginId ? `/api/${pluginId}` : '';
|
||||
return {
|
||||
use(handler: Handler) {
|
||||
|
||||
@@ -14,38 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createRootLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
createServiceFactory,
|
||||
Logger,
|
||||
loggerServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
rootLoggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Logger as WinstonLogger } from 'winston';
|
||||
|
||||
class BackstageLogger implements Logger {
|
||||
static fromWinston(logger: WinstonLogger): BackstageLogger {
|
||||
return new BackstageLogger(logger);
|
||||
}
|
||||
|
||||
private constructor(private readonly winston: WinstonLogger) {}
|
||||
|
||||
info(message: string, ...meta: any[]): void {
|
||||
this.winston.info(message, ...meta);
|
||||
}
|
||||
|
||||
child(fields: { [name: string]: string }): Logger {
|
||||
return new BackstageLogger(this.winston.child(fields));
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const loggerFactory = createServiceFactory({
|
||||
service: loggerServiceRef,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
const root = BackstageLogger.fromWinston(createRootLogger());
|
||||
return async (pluginId: string) => {
|
||||
return root.child({ pluginId });
|
||||
deps: {
|
||||
rootLogger: rootLoggerServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
async factory({ rootLogger }) {
|
||||
return async ({ plugin }) => {
|
||||
return rootLogger.child({ pluginId: plugin.getId() });
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,20 +27,16 @@ import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
export const permissionsFactory = createServiceFactory({
|
||||
service: permissionsServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
discoveryFactory: discoveryServiceRef,
|
||||
tokenManagerFactory: tokenManagerServiceRef,
|
||||
config: configServiceRef,
|
||||
discovery: discoveryServiceRef,
|
||||
tokenManager: tokenManagerServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory, discoveryFactory, tokenManagerFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
const discovery = await discoveryFactory('root');
|
||||
const tokenManager = await tokenManagerFactory('root');
|
||||
const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
return async (_pluginId: string) => {
|
||||
return permissions;
|
||||
async factory({ config }) {
|
||||
return async ({ discovery, tokenManager }) => {
|
||||
return ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2022 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 { createRootLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
createServiceFactory,
|
||||
Logger,
|
||||
rootLoggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Logger as WinstonLogger } from 'winston';
|
||||
|
||||
class BackstageLogger implements Logger {
|
||||
static fromWinston(logger: WinstonLogger): BackstageLogger {
|
||||
return new BackstageLogger(logger);
|
||||
}
|
||||
|
||||
private constructor(private readonly winston: WinstonLogger) {}
|
||||
|
||||
info(message: string, ...meta: any[]): void {
|
||||
this.winston.info(message, ...meta);
|
||||
}
|
||||
|
||||
child(fields: { [name: string]: string }): Logger {
|
||||
return new BackstageLogger(this.winston.child(fields));
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const loggerFactory = createServiceFactory({
|
||||
service: rootLoggerServiceRef,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return BackstageLogger.fromWinston(createRootLogger());
|
||||
},
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
pluginMetadataServiceRef,
|
||||
schedulerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
@@ -25,13 +26,13 @@ import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
export const schedulerFactory = createServiceFactory({
|
||||
service: schedulerServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const taskScheduler = TaskScheduler.fromConfig(config);
|
||||
return async (pluginId: string) => {
|
||||
return taskScheduler.forPlugin(pluginId);
|
||||
return async ({ plugin }) => {
|
||||
return taskScheduler.forPlugin(plugin.getId());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,32 +27,11 @@ import { ServerTokenManager } from '@backstage/backend-common';
|
||||
export const tokenManagerFactory = createServiceFactory({
|
||||
service: tokenManagerServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
loggerFactory: loggerServiceRef,
|
||||
config: configServiceRef,
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory, loggerFactory }) => {
|
||||
const logger = await loggerFactory('root');
|
||||
const config = await configFactory('root');
|
||||
return async (_pluginId: string) => {
|
||||
// doesn't the logger want to be inferred from the plugin tho here?
|
||||
// maybe ... also why do we recreate it every time otherwise
|
||||
// we should memoize on a per plugin right? so I think it's should be fine to re-use the plugin one
|
||||
// we shouldn't recreate on a per plugin basis.
|
||||
// hm - on the other hand, is this really ever called more than once?
|
||||
// not this function right. should only be called when the plugin requests this serviceRef
|
||||
// yeah so no need to worry about memo probably
|
||||
// but we still want to scope the logger to the ServrTokenmanagfer>?
|
||||
// mm sure maybe
|
||||
// maybe in this case it doesn't provide so much value b
|
||||
// oh hang on - isn't it up to THE MANAGER to make a child internally if it wants to do that
|
||||
// so that it becomes a property intrinsic to that class, no matter how it's constructed
|
||||
// or is that too much responsibility for it - making the constructor complex so to speak, making it harder to tweak that behavior
|
||||
// this is not ultra efficient :)
|
||||
|
||||
// I think the naming here is wrong to be gonest
|
||||
// this isn't like the cache manager or the database manager
|
||||
// the manager name is confusuion i think
|
||||
// aye perhaps
|
||||
async factory() {
|
||||
return async ({ config, logger }) => {
|
||||
return ServerTokenManager.fromConfig(config, {
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
});
|
||||
|
||||
@@ -27,15 +27,14 @@ import {
|
||||
export const urlReaderFactory = createServiceFactory({
|
||||
service: urlReaderServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
loggerFactory: loggerServiceRef,
|
||||
config: configServiceRef,
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory, loggerFactory }) => {
|
||||
return async (pluginId: string) => {
|
||||
const logger = await loggerFactory(pluginId);
|
||||
async factory() {
|
||||
return async ({ config, logger }) => {
|
||||
return UrlReaders.default({
|
||||
config,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
config: await configFactory(pluginId),
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
@@ -50,11 +50,12 @@ export class BackendInitializer {
|
||||
if (extensionPoint) {
|
||||
result.set(name, extensionPoint);
|
||||
} else {
|
||||
const factory = await this.#serviceHolder.get(
|
||||
const impl = await this.#serviceHolder.get(
|
||||
ref as ServiceRef<unknown>,
|
||||
pluginId,
|
||||
);
|
||||
if (factory) {
|
||||
result.set(name, await factory(pluginId));
|
||||
if (impl) {
|
||||
result.set(name, impl);
|
||||
} else {
|
||||
missingRefs.add(ref);
|
||||
}
|
||||
|
||||
@@ -18,158 +18,209 @@ import {
|
||||
createServiceRef,
|
||||
createServiceFactory,
|
||||
ServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServiceRegistry } from './ServiceRegistry';
|
||||
|
||||
const ref1 = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const ref1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
});
|
||||
const sf1 = createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
return async pluginId => {
|
||||
return { x: 1, pluginId };
|
||||
async factory() {
|
||||
return async () => {
|
||||
return { x: 1 };
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const ref2 = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const ref2 = createServiceRef<{ x: number }>({
|
||||
scope: 'root',
|
||||
id: '2',
|
||||
});
|
||||
const sf2 = createServiceFactory({
|
||||
service: ref2,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
return async pluginId => {
|
||||
return { x: 2, pluginId };
|
||||
};
|
||||
async factory() {
|
||||
return { x: 2 };
|
||||
},
|
||||
});
|
||||
const sf2b = createServiceFactory({
|
||||
service: ref2,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
return async pluginId => {
|
||||
return { x: 22, pluginId };
|
||||
};
|
||||
async factory() {
|
||||
return { x: 22 };
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
const refDefault1 = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const refDefault1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async pluginId => ({ x: 10, pluginId }),
|
||||
}),
|
||||
async factory() {
|
||||
return async () => ({ x: 10 });
|
||||
},
|
||||
})(),
|
||||
});
|
||||
|
||||
const refDefault2a = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const refDefault2a = createServiceRef<{ x: number }>({
|
||||
id: '2a',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async pluginId => ({ x: 20, pluginId }),
|
||||
async factory() {
|
||||
return async () => ({ x: 20 });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const refDefault2b = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const refDefault2b = createServiceRef<{ x: number }>({
|
||||
id: '2b',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async pluginId => ({ x: 220, pluginId }),
|
||||
async factory() {
|
||||
return async () => ({ x: 220 });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
describe('ServiceRegistry', () => {
|
||||
it('should return undefined if there is no factory defined', async () => {
|
||||
const registry = new ServiceRegistry([]);
|
||||
expect(registry.get(ref1)).toBe(undefined);
|
||||
expect(registry.get(ref1, 'catalog')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return a factory for a registered ref', async () => {
|
||||
it('should return an implementation for a registered ref', async () => {
|
||||
const registry = new ServiceRegistry([sf1]);
|
||||
const factory = registry.get(ref1)!;
|
||||
expect(factory).toEqual(expect.any(Function));
|
||||
await expect(factory('catalog')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
await expect(factory('scaffolder')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'scaffolder',
|
||||
});
|
||||
expect(await factory('catalog')).toBe(await factory('catalog'));
|
||||
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 });
|
||||
await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 });
|
||||
expect(await registry.get(ref1, 'catalog')).toBe(
|
||||
await registry.get(ref1, 'catalog'),
|
||||
);
|
||||
expect(await registry.get(ref1, 'scaffolder')).toBe(
|
||||
await registry.get(ref1, 'scaffolder'),
|
||||
);
|
||||
expect(await registry.get(ref1, 'catalog')).not.toBe(
|
||||
await registry.get(ref1, 'scaffolder'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple factories with different serviceRefs', async () => {
|
||||
const registry = new ServiceRegistry([sf1, sf2]);
|
||||
const factory1 = registry.get(ref1)!;
|
||||
const factory2 = registry.get(ref2)!;
|
||||
expect(factory1).toEqual(expect.any(Function));
|
||||
expect(factory2).toEqual(expect.any(Function));
|
||||
await expect(factory1('catalog')).resolves.toEqual({
|
||||
|
||||
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
await expect(factory2('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(ref2, 'catalog')).resolves.toEqual({
|
||||
x: 2,
|
||||
});
|
||||
expect(await registry.get(ref1, 'catalog')).not.toBe(
|
||||
await registry.get(ref2, 'catalog'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be possible for root scoped services to depend on plugin scoped services', async () => {
|
||||
const factory = createServiceFactory({
|
||||
service: ref2,
|
||||
deps: { pluginDep: ref1 },
|
||||
async factory() {
|
||||
return { x: 2 };
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory, sf1]);
|
||||
await expect(registry.get(ref2, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.",
|
||||
);
|
||||
});
|
||||
|
||||
it('should be possible for plugin scoped services to depend on root scoped services', async () => {
|
||||
const factory = createServiceFactory({
|
||||
service: ref1,
|
||||
deps: { rootDep: ref2 },
|
||||
async factory({ rootDep }) {
|
||||
return async () => ({ x: rootDep.x });
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory, sf2]);
|
||||
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
|
||||
x: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible for root scoped services to depend on root scoped services', async () => {
|
||||
const ref = createServiceRef<{ x: number }>({ id: 'x', scope: 'root' });
|
||||
const factory = createServiceFactory({
|
||||
service: ref,
|
||||
deps: { rootDep: ref2 },
|
||||
async factory({ rootDep }) {
|
||||
return { x: rootDep.x };
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory, sf2]);
|
||||
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
|
||||
x: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the pluginId from the pluginMetadata service', async () => {
|
||||
const ref = createServiceRef<{ pluginId: string }>({ id: 'x' });
|
||||
const factory = createServiceFactory({
|
||||
service: ref,
|
||||
deps: { meta: pluginMetadataServiceRef },
|
||||
async factory() {
|
||||
return async ({ meta }) => ({ pluginId: meta.getId() });
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory]);
|
||||
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
expect(await factory1('catalog')).not.toBe(await factory2('catalog'));
|
||||
});
|
||||
|
||||
it('should use the last factory for each ref', async () => {
|
||||
const registry = new ServiceRegistry([sf2, sf2b]);
|
||||
const factory2 = registry.get(ref2)!;
|
||||
await expect(factory2('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(ref2, 'catalog')).resolves.toEqual({
|
||||
x: 22,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the defaultFactory from the ref if not provided to the registry', async () => {
|
||||
it('should use the defaultFactory from the ref if not provided to the registry', async () => {
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factory = registry.get(refDefault1)!;
|
||||
expect(factory).toEqual(expect.any(Function));
|
||||
await expect(factory('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
|
||||
x: 10,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return the defaultFactory from the ref if provided to the registry', async () => {
|
||||
it('should not use the defaultFactory from the ref if provided to the registry', async () => {
|
||||
const registry = new ServiceRegistry([sf1]);
|
||||
const factory = registry.get(refDefault1)!;
|
||||
expect(factory).toEqual(expect.any(Function));
|
||||
await expect(factory('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle duplicate defaultFactories by duplicating the implementations', async () => {
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factoryA = registry.get(refDefault2a)!;
|
||||
const factoryB = registry.get(refDefault2b)!;
|
||||
expect(factoryA).toEqual(expect.any(Function));
|
||||
expect(factoryB).toEqual(expect.any(Function));
|
||||
await expect(factoryA('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault2a, 'catalog')).resolves.toEqual({
|
||||
x: 20,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
await expect(factoryB('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault2b, 'catalog')).resolves.toEqual({
|
||||
x: 220,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
expect(await factoryA('catalog')).toBe(await factoryA('catalog'));
|
||||
expect(await factoryB('catalog')).toBe(await factoryB('catalog'));
|
||||
expect(await factoryA('catalog')).not.toBe(await factoryB('catalog'));
|
||||
expect(await registry.get(refDefault2a, 'catalog')).toBe(
|
||||
await registry.get(refDefault2a, 'catalog'),
|
||||
);
|
||||
expect(await registry.get(refDefault2b, 'catalog')).toBe(
|
||||
await registry.get(refDefault2b, 'catalog'),
|
||||
);
|
||||
expect(await registry.get(refDefault2a, 'catalog')).not.toBe(
|
||||
await registry.get(refDefault2b, 'catalog'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should only call each default factory loader once', async () => {
|
||||
@@ -177,7 +228,9 @@ describe('ServiceRegistry', () => {
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async () => {},
|
||||
async factory() {
|
||||
return async () => {};
|
||||
},
|
||||
}),
|
||||
);
|
||||
const ref = createServiceRef<void>({
|
||||
@@ -186,17 +239,16 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factory = registry.get(ref)!;
|
||||
await Promise.all([
|
||||
expect(factory('catalog')).resolves.toBeUndefined(),
|
||||
expect(factory('catalog')).resolves.toBeUndefined(),
|
||||
expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(),
|
||||
expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(),
|
||||
]);
|
||||
expect(factoryLoader).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call factory functions more than once', async () => {
|
||||
const innerFactory = jest.fn(async (pluginId: string) => {
|
||||
return { x: 1, pluginId };
|
||||
const innerFactory = jest.fn(async () => {
|
||||
return { x: 1 };
|
||||
});
|
||||
const factory = jest.fn(async () => innerFactory);
|
||||
const myFactory = createServiceFactory({
|
||||
@@ -208,17 +260,15 @@ describe('ServiceRegistry', () => {
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
|
||||
await Promise.all([
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('scaffolder')!,
|
||||
registry.get(ref1)!('scaffolder')!,
|
||||
registry.get(ref1, 'catalog')!,
|
||||
registry.get(ref1, 'catalog')!,
|
||||
registry.get(ref1, 'catalog')!,
|
||||
registry.get(ref1, 'scaffolder')!,
|
||||
registry.get(ref1, 'scaffolder')!,
|
||||
]);
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(innerFactory).toHaveBeenCalledTimes(2);
|
||||
expect(innerFactory).toHaveBeenCalledWith('catalog');
|
||||
expect(innerFactory).toHaveBeenCalledWith('scaffolder');
|
||||
});
|
||||
|
||||
it('should throw if dependencies are not available', async () => {
|
||||
@@ -231,9 +281,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
const factory = registry.get(ref1)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' for 'catalog' because the following dependent services are missing: '2'",
|
||||
);
|
||||
});
|
||||
@@ -247,8 +296,8 @@ describe('ServiceRegistry', () => {
|
||||
const factoryA = createServiceFactory({
|
||||
service: refA,
|
||||
deps: { b: refB },
|
||||
async factory({ b }) {
|
||||
return async pluginId => b(pluginId);
|
||||
async factory() {
|
||||
return async ({ b }) => b;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -261,9 +310,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([factoryA, factoryB]);
|
||||
const factory = registry.get(refA)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(refA, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service 'a' for 'catalog' because the factory function threw an error, Error: Failed to instantiate service 'b' for 'catalog' because the following dependent services are missing: 'c', 'd'",
|
||||
);
|
||||
});
|
||||
@@ -278,9 +326,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
const factory = registry.get(ref1)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' because the top-level factory function threw an error, Error: top-level error",
|
||||
);
|
||||
});
|
||||
@@ -290,17 +337,16 @@ describe('ServiceRegistry', () => {
|
||||
service: ref1,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return pluginId => {
|
||||
throw new Error(`error in plugin ${pluginId}`);
|
||||
return () => {
|
||||
throw new Error(`error in plugin`);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
const factory = registry.get(ref1)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin catalog",
|
||||
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -313,9 +359,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factory = registry.get(ref)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(ref, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' because the default factory loader threw an error, Error: default factory error",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import {
|
||||
ServiceFactory,
|
||||
FactoryFunc,
|
||||
ServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
|
||||
@@ -37,7 +37,9 @@ export class ServiceRegistry {
|
||||
readonly #implementations: Map<
|
||||
ServiceFactory,
|
||||
{
|
||||
factoryFunc: Promise<FactoryFunc<unknown>>;
|
||||
factoryFunc: Promise<
|
||||
(deps: { [name in string]: unknown }) => Promise<unknown>
|
||||
>;
|
||||
byPlugin: Map<string, Promise<unknown>>;
|
||||
}
|
||||
>;
|
||||
@@ -58,67 +60,123 @@ export class ServiceRegistry {
|
||||
this.#implementations = new Map();
|
||||
}
|
||||
|
||||
get<T>(ref: ServiceRef<T>): FactoryFunc<T> | undefined {
|
||||
let factory = this.#providedFactories.get(ref.id);
|
||||
const { __defaultFactory: defaultFactory } = ref as InternalServiceRef<T>;
|
||||
if (!factory && !defaultFactory) {
|
||||
#resolveFactory(
|
||||
ref: ServiceRef<unknown>,
|
||||
pluginId: string,
|
||||
): Promise<ServiceFactory> | undefined {
|
||||
// Special case handling of the plugin metadata service, generating a custom factory for it each time
|
||||
if (ref.id === pluginMetadataServiceRef.id) {
|
||||
return Promise.resolve({
|
||||
scope: 'plugin',
|
||||
service: pluginMetadataServiceRef,
|
||||
deps: {},
|
||||
factory: async () => async () => ({
|
||||
getId() {
|
||||
return pluginId;
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let resolvedFactory: Promise<ServiceFactory> | ServiceFactory | undefined =
|
||||
this.#providedFactories.get(ref.id);
|
||||
const { __defaultFactory: defaultFactory } =
|
||||
ref as InternalServiceRef<unknown>;
|
||||
if (!resolvedFactory && !defaultFactory) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return async (pluginId: string): Promise<T> => {
|
||||
if (!factory) {
|
||||
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!);
|
||||
if (!loadedFactory) {
|
||||
loadedFactory = Promise.resolve()
|
||||
.then(() => defaultFactory!(ref))
|
||||
.then(f =>
|
||||
typeof f === 'function' ? f() : f,
|
||||
) as Promise<ServiceFactory>;
|
||||
this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory);
|
||||
}
|
||||
// NOTE: This await is safe as long as #providedFactories is not mutated.
|
||||
factory = await loadedFactory.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' because the default factory loader threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
if (!resolvedFactory) {
|
||||
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!);
|
||||
if (!loadedFactory) {
|
||||
loadedFactory = Promise.resolve()
|
||||
.then(() => defaultFactory!(ref))
|
||||
.then(f =>
|
||||
typeof f === 'function' ? f() : f,
|
||||
) as Promise<ServiceFactory>;
|
||||
this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory);
|
||||
}
|
||||
resolvedFactory = loadedFactory.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' because the default factory loader threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(resolvedFactory);
|
||||
}
|
||||
|
||||
#separateMapForTheRootService = new Map<ServiceFactory, Promise<unknown>>();
|
||||
|
||||
#checkForMissingDeps(factory: ServiceFactory, pluginId: string) {
|
||||
const missingDeps = Object.values(factory.deps).filter(ref => {
|
||||
if (ref.id === pluginMetadataServiceRef.id) {
|
||||
return false;
|
||||
}
|
||||
if (this.#providedFactories.get(ref.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(ref as InternalServiceRef<unknown>).__defaultFactory;
|
||||
});
|
||||
|
||||
if (missingDeps.length) {
|
||||
const missing = missingDeps.map(r => `'${r.id}'`).join(', ');
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(ref: ServiceRef<T>, pluginId: string): Promise<T> | undefined {
|
||||
return this.#resolveFactory(ref, pluginId)?.then(factory => {
|
||||
if (factory.scope === 'root') {
|
||||
let existing = this.#separateMapForTheRootService.get(factory);
|
||||
if (!existing) {
|
||||
this.#checkForMissingDeps(factory, pluginId);
|
||||
const rootDeps = new Array<Promise<[name: string, impl: unknown]>>();
|
||||
|
||||
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
||||
if (serviceRef.scope !== 'root') {
|
||||
throw new Error(
|
||||
`Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`,
|
||||
);
|
||||
}
|
||||
const target = this.get(serviceRef, pluginId)!;
|
||||
rootDeps.push(target.then(impl => [name, impl]));
|
||||
}
|
||||
|
||||
existing = Promise.all(rootDeps).then(entries =>
|
||||
factory.factory(Object.fromEntries(entries)),
|
||||
);
|
||||
});
|
||||
this.#separateMapForTheRootService.set(factory, existing);
|
||||
}
|
||||
return existing as Promise<T>;
|
||||
}
|
||||
|
||||
let implementation = this.#implementations.get(factory);
|
||||
if (!implementation) {
|
||||
const missingRefs = new Array<ServiceRef<unknown>>();
|
||||
const factoryDeps: { [name in string]: FactoryFunc<unknown> } = {};
|
||||
this.#checkForMissingDeps(factory, pluginId);
|
||||
const rootDeps = new Array<Promise<[name: string, impl: unknown]>>();
|
||||
|
||||
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
||||
const target = this.get(serviceRef);
|
||||
if (!target) {
|
||||
missingRefs.push(serviceRef);
|
||||
} else {
|
||||
factoryDeps[name] = target;
|
||||
if (serviceRef.scope === 'root') {
|
||||
const target = this.get(serviceRef, pluginId)!;
|
||||
rootDeps.push(target.then(impl => [name, impl]));
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRefs.length) {
|
||||
const missing = missingRefs.map(r => `'${r.id}'`).join(', ');
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${ref.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`,
|
||||
);
|
||||
}
|
||||
|
||||
implementation = {
|
||||
factoryFunc: Promise.resolve()
|
||||
.then(() => factory!.factory(factoryDeps))
|
||||
factoryFunc: Promise.all(rootDeps)
|
||||
.then(entries => factory.factory(Object.fromEntries(entries)))
|
||||
.catch(error => {
|
||||
const cause = stringifyError(error);
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' because the top-level factory function threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
`Failed to instantiate service '${ref.id}' because the top-level factory function threw an error, ${cause}`,
|
||||
);
|
||||
}),
|
||||
byPlugin: new Map(),
|
||||
@@ -129,24 +187,29 @@ export class ServiceRegistry {
|
||||
|
||||
let result = implementation.byPlugin.get(pluginId) as Promise<any>;
|
||||
if (!result) {
|
||||
result = implementation.factoryFunc.then(func =>
|
||||
Promise.resolve()
|
||||
.then(() => func(pluginId))
|
||||
.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' for '${pluginId}' because the factory function threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
const allDeps = new Array<Promise<[name: string, impl: unknown]>>();
|
||||
|
||||
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
||||
const target = this.get(serviceRef, pluginId)!;
|
||||
allDeps.push(target.then(impl => [name, impl]));
|
||||
}
|
||||
|
||||
result = implementation.factoryFunc
|
||||
.then(func =>
|
||||
Promise.all(allDeps).then(entries =>
|
||||
func(Object.fromEntries(entries)),
|
||||
),
|
||||
)
|
||||
.catch(error => {
|
||||
const cause = stringifyError(error);
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`,
|
||||
);
|
||||
});
|
||||
implementation.byPlugin.set(pluginId, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
ServiceFactory,
|
||||
BackendFeature,
|
||||
ExtensionPoint,
|
||||
FactoryFunc,
|
||||
ServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BackstageBackend } from './BackstageBackend';
|
||||
@@ -47,7 +46,7 @@ export interface CreateSpecializedBackendOptions {
|
||||
}
|
||||
|
||||
export type ServiceHolder = {
|
||||
get<T>(api: ServiceRef<T>): FactoryFunc<T> | undefined;
|
||||
get<T>(api: ServiceRef<T>, pluginId: string): Promise<T> | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,10 +66,10 @@ export interface BackendRegistrationPoints {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const cacheServiceRef: ServiceRef<PluginCacheManager>;
|
||||
export const cacheServiceRef: ServiceRef<PluginCacheManager, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const configServiceRef: ServiceRef<Config>;
|
||||
export const configServiceRef: ServiceRef<Config, 'root'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createBackendModule<
|
||||
@@ -105,22 +105,25 @@ export function createExtensionPoint<T>(options: {
|
||||
// @public (undocumented)
|
||||
export function createServiceFactory<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin',
|
||||
TImpl extends TService,
|
||||
TDeps extends {
|
||||
[name in string]: unknown;
|
||||
[name in string]: ServiceRef<unknown>;
|
||||
},
|
||||
TOpts extends
|
||||
| {
|
||||
[name in string]: unknown;
|
||||
}
|
||||
| undefined = undefined,
|
||||
>(factory: {
|
||||
service: ServiceRef<TService>;
|
||||
deps: TypesToServiceRef<TDeps>;
|
||||
>(config: {
|
||||
service: ServiceRef<TService, TScope>;
|
||||
deps: TDeps;
|
||||
factory(
|
||||
deps: DepsToDepFactories<TDeps>,
|
||||
deps: ServiceRefsToInstances<TDeps, 'root'>,
|
||||
options: TOpts,
|
||||
): Promise<FactoryFunc<TImpl>>;
|
||||
): TScope extends 'root'
|
||||
? Promise<TImpl>
|
||||
: Promise<(deps: ServiceRefsToInstances<TDeps>) => Promise<TImpl>>;
|
||||
}): undefined extends TOpts
|
||||
? (options?: TOpts) => ServiceFactory<TService>
|
||||
: (options: TOpts) => ServiceFactory<TService>;
|
||||
@@ -128,21 +131,26 @@ export function createServiceFactory<
|
||||
// @public (undocumented)
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope?: 'plugin';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
service: ServiceRef<T, 'plugin'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T>;
|
||||
}): ServiceRef<T, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const databaseServiceRef: ServiceRef<PluginDatabaseManager>;
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope: 'root';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T, 'root'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T, 'root'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type DepsToDepFactories<T> = {
|
||||
[key in keyof T]: (pluginId: string) => Promise<T[key]>;
|
||||
};
|
||||
export const databaseServiceRef: ServiceRef<PluginDatabaseManager, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const discoveryServiceRef: ServiceRef<PluginEndpointDiscovery>;
|
||||
export const discoveryServiceRef: ServiceRef<PluginEndpointDiscovery, 'plugin'>;
|
||||
|
||||
// @public
|
||||
export type ExtensionPoint<T> = {
|
||||
@@ -152,9 +160,6 @@ export type ExtensionPoint<T> = {
|
||||
$$ref: 'extension-point';
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type FactoryFunc<Impl> = (pluginId: string) => Promise<Impl>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface HttpRouterService {
|
||||
// (undocumented)
|
||||
@@ -162,7 +167,7 @@ export interface HttpRouterService {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const httpRouterServiceRef: ServiceRef<HttpRouterService>;
|
||||
export const httpRouterServiceRef: ServiceRef<HttpRouterService, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Logger {
|
||||
@@ -173,7 +178,7 @@ export interface Logger {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const loggerServiceRef: ServiceRef<Logger>;
|
||||
export const loggerServiceRef: ServiceRef<Logger, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function loggerToWinstonLogger(
|
||||
@@ -183,33 +188,66 @@ export function loggerToWinstonLogger(
|
||||
|
||||
// @public (undocumented)
|
||||
export const permissionsServiceRef: ServiceRef<
|
||||
PermissionAuthorizer | PermissionEvaluator
|
||||
PermissionAuthorizer | PermissionEvaluator,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const schedulerServiceRef: ServiceRef<PluginTaskScheduler>;
|
||||
export interface PluginMetadata {
|
||||
// (undocumented)
|
||||
getId(): string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ServiceFactory<TService = unknown> = {
|
||||
service: ServiceRef<TService>;
|
||||
deps: {
|
||||
[key in string]: ServiceRef<unknown>;
|
||||
};
|
||||
factory(deps: {
|
||||
[key in string]: unknown;
|
||||
}): Promise<FactoryFunc<TService>>;
|
||||
};
|
||||
export const pluginMetadataServiceRef: ServiceRef<PluginMetadata, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const rootLoggerServiceRef: ServiceRef<Logger, 'root'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const schedulerServiceRef: ServiceRef<PluginTaskScheduler, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ServiceFactory<TService = unknown> =
|
||||
| {
|
||||
scope: 'root';
|
||||
service: ServiceRef<TService, 'root'>;
|
||||
deps: {
|
||||
[key in string]: ServiceRef<unknown>;
|
||||
};
|
||||
factory(deps: {
|
||||
[key in string]: unknown;
|
||||
}): Promise<TService>;
|
||||
}
|
||||
| {
|
||||
scope: 'plugin';
|
||||
service: ServiceRef<TService, 'plugin'>;
|
||||
deps: {
|
||||
[key in string]: ServiceRef<unknown>;
|
||||
};
|
||||
factory(deps: {
|
||||
[key in string]: unknown;
|
||||
}): Promise<
|
||||
(deps: {
|
||||
[key in string]: unknown;
|
||||
}) => Promise<TService>
|
||||
>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ServiceRef<T> = {
|
||||
export type ServiceRef<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
|
||||
> = {
|
||||
id: string;
|
||||
T: T;
|
||||
scope: TScope;
|
||||
T: TService;
|
||||
toString(): string;
|
||||
$$ref: 'service';
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const tokenManagerServiceRef: ServiceRef<TokenManager>;
|
||||
export const tokenManagerServiceRef: ServiceRef<TokenManager, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TypesToServiceRef<T> = {
|
||||
@@ -217,5 +255,5 @@ export type TypesToServiceRef<T> = {
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const urlReaderServiceRef: ServiceRef<UrlReader>;
|
||||
export const urlReaderServiceRef: ServiceRef<UrlReader, 'plugin'>;
|
||||
```
|
||||
|
||||
@@ -21,5 +21,6 @@ import { createServiceRef } from '../system/types';
|
||||
* @public
|
||||
*/
|
||||
export const configServiceRef = createServiceRef<Config>({
|
||||
id: 'core.config',
|
||||
id: 'core.root.config',
|
||||
scope: 'root',
|
||||
});
|
||||
|
||||
@@ -26,3 +26,6 @@ export { discoveryServiceRef } from './discoveryServiceRef';
|
||||
export { tokenManagerServiceRef } from './tokenManagerServiceRef';
|
||||
export { permissionsServiceRef } from './permissionsServiceRef';
|
||||
export { schedulerServiceRef } from './schedulerServiceRef';
|
||||
export { rootLoggerServiceRef } from './rootLoggerServiceRef';
|
||||
export { pluginMetadataServiceRef } from './pluginMetadataServiceRef';
|
||||
export type { PluginMetadata } from './pluginMetadataServiceRef';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2022 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 { createServiceRef } from '../system/types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PluginMetadata {
|
||||
getId(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const pluginMetadataServiceRef = createServiceRef<PluginMetadata>({
|
||||
id: 'core.plugin-metadata',
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2022 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 { createServiceRef } from '../system/types';
|
||||
import { Logger } from './loggerServiceRef';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const rootLoggerServiceRef = createServiceRef<Logger>({
|
||||
id: 'core.root.logger',
|
||||
scope: 'root',
|
||||
});
|
||||
@@ -14,11 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type {
|
||||
ServiceRef,
|
||||
TypesToServiceRef,
|
||||
DepsToDepFactories,
|
||||
FactoryFunc,
|
||||
ServiceFactory,
|
||||
} from './types';
|
||||
export type { ServiceRef, TypesToServiceRef, ServiceFactory } from './types';
|
||||
export { createServiceRef, createServiceFactory } from './types';
|
||||
|
||||
@@ -19,63 +19,87 @@
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ServiceRef<T> = {
|
||||
export type ServiceRef<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
|
||||
> = {
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* This determines the scope at which this service is available.
|
||||
*
|
||||
* Root scoped services are available to all other services but
|
||||
* may only depend on other root scoped services.
|
||||
*
|
||||
* Plugin scoped services are only available to other plugin scoped
|
||||
* services but may depend on all other services.
|
||||
*/
|
||||
scope: TScope;
|
||||
|
||||
/**
|
||||
* Utility for getting the type of the service, using `typeof serviceRef.T`.
|
||||
* Attempting to actually read this value will result in an exception.
|
||||
*/
|
||||
T: T;
|
||||
T: TService;
|
||||
|
||||
toString(): string;
|
||||
|
||||
$$ref: 'service';
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type InternalServiceRef<T> = ServiceRef<T> & {
|
||||
/**
|
||||
* The default factory that will be used to create service
|
||||
* instances if no other factory is provided.
|
||||
*/
|
||||
__defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type TypesToServiceRef<T> = { [key in keyof T]: ServiceRef<T[key]> };
|
||||
|
||||
/** @public */
|
||||
export type DepsToDepFactories<T> = {
|
||||
[key in keyof T]: (pluginId: string) => Promise<T[key]>;
|
||||
};
|
||||
export type ServiceFactory<TService = unknown> =
|
||||
| {
|
||||
// This scope prop is needed in addition to the service ref, as TypeScript
|
||||
// can't properly discriminate the two factory types otherwise.
|
||||
scope: 'root';
|
||||
service: ServiceRef<TService, 'root'>;
|
||||
deps: { [key in string]: ServiceRef<unknown> };
|
||||
factory(deps: { [key in string]: unknown }): Promise<TService>;
|
||||
}
|
||||
| {
|
||||
scope: 'plugin';
|
||||
service: ServiceRef<TService, 'plugin'>;
|
||||
deps: { [key in string]: ServiceRef<unknown> };
|
||||
factory(deps: { [key in string]: unknown }): Promise<
|
||||
(deps: { [key in string]: unknown }) => Promise<TService>
|
||||
>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type FactoryFunc<Impl> = (pluginId: string) => Promise<Impl>;
|
||||
|
||||
/** @public */
|
||||
export type ServiceFactory<TService = unknown> = {
|
||||
service: ServiceRef<TService>;
|
||||
deps: { [key in string]: ServiceRef<unknown> };
|
||||
factory(deps: { [key in string]: unknown }): Promise<FactoryFunc<TService>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope?: 'plugin';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
service: ServiceRef<T, 'plugin'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T, 'plugin'>;
|
||||
/** @public */
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope: 'root';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T, 'root'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T, 'root'>;
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope?: 'plugin' | 'root';
|
||||
defaultFactory?:
|
||||
| ((
|
||||
service: ServiceRef<T, 'plugin'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>)
|
||||
| ((
|
||||
service: ServiceRef<T, 'root'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>);
|
||||
}): ServiceRef<T> {
|
||||
const { id, defaultFactory } = options;
|
||||
const { id, scope = 'plugin', defaultFactory } = options;
|
||||
return {
|
||||
id,
|
||||
scope,
|
||||
get T(): T {
|
||||
throw new Error(`tried to read ServiceRef.T of ${this}`);
|
||||
},
|
||||
@@ -84,32 +108,51 @@ export function createServiceRef<T>(options: {
|
||||
},
|
||||
$$ref: 'service', // TODO: declare
|
||||
__defaultFactory: defaultFactory,
|
||||
} as InternalServiceRef<T>;
|
||||
} as ServiceRef<T, typeof scope> & {
|
||||
__defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
};
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
type ServiceRefsToInstances<
|
||||
T extends { [key in string]: ServiceRef<unknown> },
|
||||
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
|
||||
> = {
|
||||
[name in {
|
||||
[key in keyof T]: T[key] extends ServiceRef<unknown, TScope> ? key : never;
|
||||
}[keyof T]]: T[name] extends ServiceRef<infer TImpl> ? TImpl : never;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createServiceFactory<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin',
|
||||
TImpl extends TService,
|
||||
TDeps extends { [name in string]: unknown },
|
||||
TDeps extends { [name in string]: ServiceRef<unknown> },
|
||||
TOpts extends { [name in string]: unknown } | undefined = undefined,
|
||||
>(factory: {
|
||||
service: ServiceRef<TService>;
|
||||
deps: TypesToServiceRef<TDeps>;
|
||||
>(config: {
|
||||
service: ServiceRef<TService, TScope>;
|
||||
deps: TDeps;
|
||||
factory(
|
||||
deps: DepsToDepFactories<TDeps>,
|
||||
deps: ServiceRefsToInstances<TDeps, 'root'>,
|
||||
options: TOpts,
|
||||
): Promise<FactoryFunc<TImpl>>;
|
||||
): TScope extends 'root'
|
||||
? Promise<TImpl>
|
||||
: Promise<(deps: ServiceRefsToInstances<TDeps>) => Promise<TImpl>>;
|
||||
}): undefined extends TOpts
|
||||
? (options?: TOpts) => ServiceFactory<TService>
|
||||
: (options: TOpts) => ServiceFactory<TService> {
|
||||
return (options?: TOpts) => ({
|
||||
service: factory.service,
|
||||
deps: factory.deps,
|
||||
factory(deps: DepsToDepFactories<TDeps>) {
|
||||
return factory.factory(deps, options!);
|
||||
},
|
||||
});
|
||||
return (options?: TOpts) =>
|
||||
({
|
||||
scope: config.service.scope,
|
||||
service: config.service,
|
||||
deps: config.deps,
|
||||
factory(deps: ServiceRefsToInstances<TDeps, 'root'>) {
|
||||
return config.factory(deps, options!);
|
||||
},
|
||||
} as ServiceFactory<TService>);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export type CatalogProcessorResult =
|
||||
| CatalogProcessorRefreshKeysResult;
|
||||
|
||||
// @alpha
|
||||
export const catalogServiceRef: ServiceRef<CatalogApi>;
|
||||
export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin'>;
|
||||
|
||||
// @public
|
||||
export type DeferredEntity = {
|
||||
|
||||
@@ -31,13 +31,11 @@ export const catalogServiceRef = createServiceRef<CatalogApi>({
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {
|
||||
discoveryFactory: discoveryServiceRef,
|
||||
discoveryApi: discoveryServiceRef,
|
||||
},
|
||||
factory: async ({ discoveryFactory }) => {
|
||||
const discoveryApi = await discoveryFactory('root');
|
||||
const catalogClient = new CatalogClient({ discoveryApi });
|
||||
return async _pluginId => {
|
||||
return catalogClient;
|
||||
async factory() {
|
||||
return async ({ discoveryApi }) => {
|
||||
return new CatalogClient({ discoveryApi });
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user