UNPKG

@backstage/backend-test-utils

Version:

Test helpers library for Backstage backends

247 lines (243 loc) • 9.5 kB
'use strict'; 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