import 'reflect-metadata' import {collect, Collection} from '../../util/collection/Collection' import {logIfDebugging} from '../../util/support/debug' import { DEPENDENCY_KEYS_METADATA_KEY, DEPENDENCY_KEYS_SERVICE_TYPE_KEY, DependencyKey, DependencyRequirement, InjectionType, isInstantiable, PropertyDependency, } from '../types' import {ContainerBlueprint} from '../ContainerBlueprint' import {propertyInjectionMetadata} from './propertyInjectionMetadata' /** * Get a collection of dependency requirements for the given target object. * @param {Object} target * @return Collection */ function initDependencyMetadata(target: unknown): Collection { const paramTypes = Reflect.getMetadata('design:paramtypes', target as any) return collect(paramTypes).map((type, idx) => { return { paramIndex: idx, key: type, overridden: false, } }) } /** * Class decorator that marks a class as injectable. When this is applied, dependency * metadata for the constructors params is resolved and stored in metadata. * @constructor */ export const Injectable = (): ClassDecorator => { return (target) => { const meta = initDependencyMetadata(target) const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) const newMetadata = new Collection() if ( existing ) { const maxNew = meta.max('paramIndex') const maxExisting = existing.max('paramIndex') for ( let i = 0; i <= Math.max(maxNew, maxExisting); i++ ) { const existingDR = existing.firstWhere('paramIndex', '=', i) const newDR = meta.firstWhere('paramIndex', '=', i) if ( existingDR && !newDR ) { newMetadata.push(existingDR) } else if ( newDR && !existingDR ) { newMetadata.push(newDR) } else if ( newDR && existingDR ) { if ( existingDR.overridden ) { newMetadata.push(existingDR) } else { newMetadata.push(newDR) } } } } else { newMetadata.concat(meta) } Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, newMetadata, target) } } /** * Mark the given class property to be injected by the container. * If a `key` is specified, that DependencyKey will be injected. * Otherwise, the DependencyKey is inferred from the type annotation. * @param key * @param debug * @constructor */ export const Inject = (key?: DependencyKey, { debug = false } = {}): PropertyDecorator => { return (target, property) => { if ( !target?.constructor ) { logIfDebugging('extollo.di.decoration', '[DEBUG] @Inject(): target has no constructor', target) throw new Error('Unable to define property injection: target has no constructor. Enable `extollo.di.decoration` logging to debug') } const propertyTarget = target.constructor // let propertyMetadata = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, propertyTarget) as Collection // Okay, this is a little fucky. We can't use Reflect's metadata capabilities because we need to write the metadata to // the constructor, not the `target`. Because Reflect is using the prototype to store data, defining a metadata key on the constructor // will define it for its parent constructors as well. // So, if you have class A, class B extends A, and class C extends A, the properties for B and C will be defined on A, causing // BOTH B and C's properties to be injected on any class extending A. // To get around this, we instead define a custom property on the constructor itself, then use hasOwnProperty to make sure we're not // getting the one for the parent class via the prototype chain. let propertyMetadata = Object.prototype.hasOwnProperty.call(propertyTarget, propertyInjectionMetadata) ? (propertyTarget as any)[propertyInjectionMetadata] as Collection : undefined if ( !propertyMetadata ) { propertyMetadata = new Collection() ;(propertyTarget as any)[propertyInjectionMetadata] = propertyMetadata } const type = Reflect.getMetadata('design:type', target, property) if ( !key && type ) { key = type } if ( key ) { const existing = propertyMetadata.firstWhere('property', '=', property) if ( existing ) { existing.key = key } else { propertyMetadata.push({ property, key, debug, }) } } logIfDebugging('extollo.di.decoration', '[DEBUG] @Inject() - key:', key, 'property:', property, 'target:', target, 'target constructor:', target?.constructor, 'type:', type) ;(propertyTarget as any)[propertyInjectionMetadata] = propertyMetadata } } /** * Parameter decorator to manually mark a parameter as being an injection target on injectable * classes. This can be used to override the dependency key of a given parameter. * @param {DependencyKey} key * @constructor */ export const InjectParam = (key: DependencyKey): ParameterDecorator => { return (target, property, paramIndex) => { if ( !Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) ) { Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, initDependencyMetadata(target), target) } const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) const req = meta.firstWhere('paramIndex', '=', paramIndex) if ( req ) { req.key = key req.overridden = true } else { meta.push({ paramIndex, key, overridden: true, }) } Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, meta, target) } } /** * Class decorator that registers the class as a singleton instance in the global container. * @param {string} name */ export const Singleton = (name?: string): ClassDecorator => { return (target) => { if ( isInstantiable(target) ) { const injectionType: InjectionType = { type: name ? 'named' : 'singleton', ...(name ? { name } : {}), } logIfDebugging('extollo.di.singleton', 'Registering singleton target:', target, 'injectionType:', injectionType) Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target) Injectable()(target) if ( name ) { ContainerBlueprint.getContainerBlueprint().registerNamed(name, target) } else { ContainerBlueprint.getContainerBlueprint().register(target) } } } } /** * Register a factory class directly with any created containers. * @constructor */ export const FactoryProducer = (): ClassDecorator => { return (target) => { logIfDebugging('extollo.di.injector', 'Registering factory producer for target:', target) if ( isInstantiable(target) ) { ContainerBlueprint.getContainerBlueprint().registerFactory(target) } } }