|
|
|
import 'reflect-metadata'
|
|
|
|
import {collect, Collection} from '../../util'
|
|
|
|
import {logIfDebugging} from '../../util/support/debug'
|
|
|
|
import {
|
|
|
|
DependencyKey,
|
|
|
|
DependencyRequirement,
|
|
|
|
DEPENDENCY_KEYS_METADATA_KEY,
|
|
|
|
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
|
|
|
|
isInstantiable,
|
|
|
|
InjectionType,
|
|
|
|
DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
|
|
|
|
PropertyDependency,
|
|
|
|
} from '../types'
|
|
|
|
import {ContainerBlueprint} from '../ContainerBlueprint'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a collection of dependency requirements for the given target object.
|
|
|
|
* @param {Object} target
|
|
|
|
* @return Collection<DependencyRequirement>
|
|
|
|
*/
|
|
|
|
function initDependencyMetadata(target: unknown): Collection<DependencyRequirement> {
|
|
|
|
const paramTypes = Reflect.getMetadata('design:paramtypes', target as any)
|
|
|
|
return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((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<DependencyRequirement>()
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
let propertyMetadata = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, target?.constructor || target) as Collection<PropertyDependency>
|
|
|
|
if ( !propertyMetadata ) {
|
|
|
|
propertyMetadata = new Collection<PropertyDependency>()
|
|
|
|
Reflect.defineMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, propertyMetadata, target?.constructor || target)
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
Reflect.defineMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, propertyMetadata, target?.constructor || target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 } : {}),
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|