@backstage/backend-test-utils
Version:
Test helpers library for Backstage backends
247 lines (243 loc) • 9.5 kB
JavaScript
;
var backendPluginApi = require('@backstage/backend-plugin-api');
var errors = require('@backstage/errors');
var DependencyGraph = require('../lib/DependencyGraph.cjs.js');
function toInternalServiceFactory(factory) {
const f = factory;
if (f.$$type !== "@backstage/BackendFeature") {
throw new Error(`Invalid service factory, bad type '${f.$$type}'`);
}
if (f.version !== "v1") {
throw new Error(`Invalid service factory, bad version '${f.version}'`);
}
return f;
}
function createPluginMetadataServiceFactory(pluginId) {
return backendPluginApi.createServiceFactory({
service: backendPluginApi.coreServices.pluginMetadata,
deps: {},
factory: async () => ({ getId: () => pluginId })
});
}
class ServiceRegistry {
static create(factories) {
const factoryMap = /* @__PURE__ */ new Map();
for (const factory of factories) {
if (factory.service.multiton) {
const existing = factoryMap.get(factory.service.id) ?? [];
factoryMap.set(
factory.service.id,
existing.concat(toInternalServiceFactory(factory))
);
} else {
factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]);
}
}
const registry = new ServiceRegistry(factoryMap);
registry.checkForCircularDeps();
return registry;
}
#providedFactories;
#loadedDefaultFactories;
#implementations;
#rootServiceImplementations = /* @__PURE__ */ new Map();
#addedFactoryIds = /* @__PURE__ */ new Set();
#instantiatedFactories = /* @__PURE__ */ new Set();
constructor(factories) {
this.#providedFactories = factories;
this.#loadedDefaultFactories = /* @__PURE__ */ new Map();
this.#implementations = /* @__PURE__ */ new Map();
}
#resolveFactory(ref, pluginId) {
if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
return Promise.resolve([
toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId))
]);
}
let resolvedFactory = this.#providedFactories.get(ref.id);
const { __defaultFactory: defaultFactory } = ref;
if (!resolvedFactory && !defaultFactory) {
return void 0;
}
if (!resolvedFactory) {
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory);
if (!loadedFactory) {
loadedFactory = Promise.resolve().then(() => defaultFactory(ref)).then(
(f) => toInternalServiceFactory(typeof f === "function" ? f() : f)
);
this.#loadedDefaultFactories.set(defaultFactory, loadedFactory);
}
resolvedFactory = loadedFactory.then(
(factory) => [factory],
(error) => {
throw new Error(
`Failed to instantiate service '${ref.id}' because the default factory loader threw an error, ${errors.stringifyError(
error
)}`
);
}
);
}
return Promise.resolve(resolvedFactory);
}
#checkForMissingDeps(factory, pluginId) {
const missingDeps = Object.values(factory.deps).filter((ref) => {
if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
return false;
}
if (this.#providedFactories.get(ref.id)) {
return false;
}
if (ref.multiton) {
return false;
}
return !ref.__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}`
);
}
}
checkForCircularDeps() {
const graph = DependencyGraph.DependencyGraph.fromIterable(
Array.from(this.#providedFactories).map(([serviceId, factories]) => ({
value: serviceId,
provides: [serviceId],
consumes: factories.flatMap(
(factory) => Object.values(factory.deps).map((d) => d.id)
)
}))
);
const circularDependencies = Array.from(graph.detectCircularDependencies());
if (circularDependencies.length) {
const cycles = circularDependencies.map((c) => c.map((id) => `'${id}'`).join(" -> ")).join("\n ");
throw new errors.ConflictError(`Circular dependencies detected:
${cycles}`);
}
}
hasBeenAdded(ref) {
if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
return true;
}
return this.#addedFactoryIds.has(ref.id);
}
add(factory) {
const factoryId = factory.service.id;
if (factoryId === backendPluginApi.coreServices.pluginMetadata.id) {
throw new Error(
`The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
);
}
if (this.#instantiatedFactories.has(factoryId)) {
throw new Error(
`Unable to set service factory with id ${factoryId}, service has already been instantiated`
);
}
if (factory.service.multiton) {
const newFactories = (this.#providedFactories.get(factoryId) ?? []).concat(toInternalServiceFactory(factory));
this.#providedFactories.set(factoryId, newFactories);
} else {
if (this.#addedFactoryIds.has(factoryId)) {
throw new Error(
`Duplicate service implementations provided for ${factoryId}`
);
}
this.#addedFactoryIds.add(factoryId);
this.#providedFactories.set(factoryId, [
toInternalServiceFactory(factory)
]);
}
}
async initializeEagerServicesWithScope(scope, pluginId = "root") {
for (const [factory] of this.#providedFactories.values()) {
if (factory.service.scope === scope) {
if (scope === "root" && factory.initialization !== "lazy") {
await this.get(factory.service, pluginId);
} else if (scope === "plugin" && factory.initialization === "always") {
await this.get(factory.service, pluginId);
}
}
}
}
get(ref, pluginId) {
this.#instantiatedFactories.add(ref.id);
const resolvedFactory = this.#resolveFactory(ref, pluginId);
if (!resolvedFactory) {
return ref.multiton ? Promise.resolve([]) : void 0;
}
return resolvedFactory.then((factories) => {
return Promise.all(
factories.map((factory) => {
if (factory.service.scope === "root") {
let existing = this.#rootServiceImplementations.get(factory);
if (!existing) {
this.#checkForMissingDeps(factory, pluginId);
const rootDeps = new Array();
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), void 0)
);
this.#rootServiceImplementations.set(factory, existing);
}
return existing;
}
let implementation = this.#implementations.get(factory);
if (!implementation) {
this.#checkForMissingDeps(factory, pluginId);
const rootDeps = new Array();
for (const [name, serviceRef] of Object.entries(factory.deps)) {
if (serviceRef.scope === "root") {
const target = this.get(serviceRef, pluginId);
rootDeps.push(target.then((impl) => [name, impl]));
}
}
implementation = {
context: Promise.all(rootDeps).then(
(entries) => factory.createRootContext?.(Object.fromEntries(entries))
).catch((error) => {
const cause = errors.stringifyError(error);
throw new Error(
`Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`
);
}),
byPlugin: /* @__PURE__ */ new Map()
};
this.#implementations.set(factory, implementation);
}
let result = implementation.byPlugin.get(pluginId);
if (!result) {
const allDeps = new Array();
for (const [name, serviceRef] of Object.entries(factory.deps)) {
const target = this.get(serviceRef, pluginId);
allDeps.push(target.then((impl) => [name, impl]));
}
result = implementation.context.then(
(context) => Promise.all(allDeps).then(
(entries) => factory.factory(Object.fromEntries(entries), context)
)
).catch((error) => {
const cause = errors.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;
})
);
}).then((results) => ref.multiton ? results : results[0]);
}
}
exports.ServiceRegistry = ServiceRegistry;
//# sourceMappingURL=ServiceRegistry.cjs.js.map