n8n-nodes-semble
Version:
n8n community node for Semble practice management system - automate bookings, patients, and product/service catalog management
273 lines (272 loc) • 9.46 kB
JavaScript
"use strict";
/**
* @fileoverview ServiceContainer.ts
* @description Dependency injection container for clean architecture and testability. Provides service registration, resolution, and lifecycle management.
* @author Mike Hatcher
* @website https://progenious.com
* @namespace N8nNodesSemble.Core.ServiceContainer
* @since 2.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServiceContainerUtils = exports.serviceContainer = exports.ServiceContainer = exports.ServiceLifetime = void 0;
exports.Service = Service;
exports.Injectable = Injectable;
const SembleError_1 = require("./SembleError");
/**
* Service lifecycle types
*/
var ServiceLifetime;
(function (ServiceLifetime) {
ServiceLifetime["SINGLETON"] = "singleton";
ServiceLifetime["TRANSIENT"] = "transient";
ServiceLifetime["SCOPED"] = "scoped";
})(ServiceLifetime || (exports.ServiceLifetime = ServiceLifetime = {}));
/**
* Dependency injection container implementation
* Supports singleton, transient, and scoped service lifetimes
*/
class ServiceContainer {
constructor() {
this.services = new Map();
this.singletonInstances = new Map();
this.scopedInstances = new Map();
this.resolutionStack = [];
}
/**
* Register a service with the container
*/
register(name, factory, lifetime = ServiceLifetime.TRANSIENT, dependencies = []) {
if (this.services.has(name)) {
throw new SembleError_1.SembleError(`Service '${name}' is already registered`);
}
this.services.set(name, {
name,
factory,
lifetime,
dependencies
});
}
/**
* Register a singleton service
*/
registerSingleton(name, factory, dependencies = []) {
this.register(name, factory, ServiceLifetime.SINGLETON, dependencies);
}
/**
* Register a transient service
*/
registerTransient(name, factory, dependencies = []) {
this.register(name, factory, ServiceLifetime.TRANSIENT, dependencies);
}
/**
* Register a scoped service
*/
registerScoped(name, factory, dependencies = []) {
this.register(name, factory, ServiceLifetime.SCOPED, dependencies);
}
/**
* Resolve a service by name
*/
resolve(name, scope) {
// Check for circular dependencies
if (this.resolutionStack.includes(name)) {
const cycle = [...this.resolutionStack, name].join(' -> ');
throw new SembleError_1.SembleError(`Circular dependency detected: ${cycle}`);
}
const registration = this.services.get(name);
if (!registration) {
throw new SembleError_1.SembleError(`Service '${name}' is not registered`);
}
this.resolutionStack.push(name);
try {
switch (registration.lifetime) {
case ServiceLifetime.SINGLETON:
return this.resolveSingleton(registration);
case ServiceLifetime.SCOPED:
return this.resolveScoped(registration, scope || 'default');
case ServiceLifetime.TRANSIENT:
default:
return this.resolveTransient(registration, scope);
}
}
finally {
this.resolutionStack.pop();
}
}
/**
* Resolve singleton service
*/
resolveSingleton(registration) {
if (this.singletonInstances.has(registration.name)) {
return this.singletonInstances.get(registration.name);
}
const dependencies = this.resolveDependencies(registration.dependencies || []);
const instance = registration.factory(...dependencies);
this.singletonInstances.set(registration.name, instance);
return instance;
}
/**
* Resolve scoped service
*/
resolveScoped(registration, scope) {
if (!this.scopedInstances.has(scope)) {
this.scopedInstances.set(scope, new Map());
}
const scopeInstances = this.scopedInstances.get(scope);
if (scopeInstances.has(registration.name)) {
return scopeInstances.get(registration.name);
}
const dependencies = this.resolveDependencies(registration.dependencies || [], scope);
const instance = registration.factory(...dependencies);
scopeInstances.set(registration.name, instance);
return instance;
}
/**
* Resolve transient service
*/
resolveTransient(registration, scope) {
const dependencies = this.resolveDependencies(registration.dependencies || [], scope);
return registration.factory(...dependencies);
}
/**
* Resolve service dependencies
*/
resolveDependencies(dependencies, scope) {
return dependencies.map(dep => this.resolve(dep, scope));
}
/**
* Check if a service is registered
*/
isRegistered(name) {
return this.services.has(name);
}
/**
* Get all registered service names
*/
getRegisteredServices() {
return Array.from(this.services.keys());
}
/**
* Clear all services and instances
*/
clear() {
this.services.clear();
this.singletonInstances.clear();
this.scopedInstances.clear();
this.resolutionStack = [];
}
/**
* Clear scoped instances for a specific scope
*/
clearScope(scope) {
this.scopedInstances.delete(scope);
}
/**
* Create a scoped container
*/
createScope(scopeName) {
return new ScopedServiceContainer(this, scopeName);
}
/**
* Get service registration info
*/
getServiceInfo(name) {
return this.services.get(name);
}
/**
* Register multiple services at once
*/
registerBatch(registrations) {
for (const reg of registrations) {
this.register(reg.name, reg.factory, reg.lifetime || ServiceLifetime.TRANSIENT, reg.dependencies || []);
}
}
}
exports.ServiceContainer = ServiceContainer;
/**
* Scoped service container implementation
*/
class ScopedServiceContainer {
constructor(parentContainer, scopeName) {
this.parentContainer = parentContainer;
this.scopeName = scopeName;
}
register(name, factory, lifetime, dependencies) {
this.parentContainer.register(name, factory, lifetime, dependencies);
}
resolve(name) {
return this.parentContainer.resolve(name, this.scopeName);
}
isRegistered(name) {
return this.parentContainer.isRegistered(name);
}
clear() {
this.parentContainer.clearScope(this.scopeName);
}
createScope(scopeName) {
return this.parentContainer.createScope(scopeName);
}
}
/**
* Default service container instance
*/
exports.serviceContainer = new ServiceContainer();
/**
* Service decorator for automatic registration
*/
function Service(name, lifetime = ServiceLifetime.TRANSIENT) {
return function (constructor) {
exports.serviceContainer.register(name, () => new constructor(), lifetime);
return constructor;
};
}
/**
* Injectable decorator for dependency injection
*/
function Injectable(dependencies = []) {
return function (constructor) {
const originalConstructor = constructor;
const injectedConstructor = function (...args) {
const resolvedDependencies = dependencies.map(dep => exports.serviceContainer.resolve(dep));
return new originalConstructor(...resolvedDependencies, ...args);
};
injectedConstructor.prototype = originalConstructor.prototype;
return injectedConstructor;
};
}
/**
* Utility functions for common service patterns
*/
class ServiceContainerUtils {
/**
* Register all Semble core services
*/
static registerCoreServices(container) {
// This will be called from the main node files to register all services
// Services will be registered in dependency order
// Core services (no dependencies)
container.registerSingleton('config', () => ({}), []);
container.registerSingleton('cache', () => ({}), ['config']);
// API services
container.registerSingleton('credentials', () => ({}), ['config']);
container.registerSingleton('queryService', () => ({}), ['credentials', 'cache']);
container.registerSingleton('fieldDiscovery', () => ({}), ['queryService', 'cache']);
container.registerSingleton('permissionCheck', () => ({}), ['queryService', 'cache']);
container.registerSingleton('validation', () => ({}), ['fieldDiscovery']);
// Component services
container.registerTransient('resourceSelector', () => ({}), ['fieldDiscovery', 'cache']);
container.registerTransient('eventTrigger', () => ({}), []);
container.registerTransient('eventAction', () => ({}), []);
container.registerTransient('pollTime', () => ({}), []);
container.registerSingleton('additionalFields', () => ({}), ['fieldDiscovery']);
}
/**
* Create a service container with all Semble services registered
*/
static createConfiguredContainer() {
const container = new ServiceContainer();
ServiceContainerUtils.registerCoreServices(container);
return container;
}
}
exports.ServiceContainerUtils = ServiceContainerUtils;