Files
lib/src/di/decorator/injection.ts

155 lines
5.3 KiB
TypeScript
Raw Normal View History

2021-06-01 20:59:40 -05:00
import 'reflect-metadata'
2021-06-02 22:36:25 -05:00
import {collect, Collection} from '../../util'
2021-06-01 20:59:40 -05:00
import {
DependencyKey,
DependencyRequirement,
DEPENDENCY_KEYS_METADATA_KEY,
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
isInstantiable,
InjectionType,
DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
PropertyDependency,
2021-06-02 22:36:25 -05:00
} from '../types'
import {ContainerBlueprint} from '../ContainerBlueprint'
2021-06-01 20:59:40 -05:00
/**
* Get a collection of dependency requirements for the given target object.
* @param {Object} target
* @return Collection<DependencyRequirement>
*/
2021-06-02 22:36:25 -05:00
function initDependencyMetadata(target: unknown): Collection<DependencyRequirement> {
const paramTypes = Reflect.getMetadata('design:paramtypes', target as any)
2021-06-01 20:59:40 -05:00
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)
2021-06-02 22:36:25 -05:00
const newMetadata = new Collection<DependencyRequirement>()
2021-06-01 20:59:40 -05:00
if ( existing ) {
2021-06-02 22:36:25 -05:00
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)
2021-06-01 20:59:40 -05:00
2021-06-02 22:36:25 -05:00
if ( existingDR && !newDR ) {
newMetadata.push(existingDR)
} else if ( newDR && !existingDR ) {
newMetadata.push(newDR)
} else if ( newDR && existingDR ) {
if ( existingDR.overridden ) {
newMetadata.push(existingDR)
2021-06-01 20:59:40 -05:00
} else {
2021-06-02 22:36:25 -05:00
newMetadata.push(newDR)
2021-06-01 20:59:40 -05:00
}
}
}
} else {
2021-06-02 22:36:25 -05:00
newMetadata.concat(meta)
2021-06-01 20:59:40 -05:00
}
2021-06-02 22:36:25 -05:00
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, newMetadata, target)
2021-06-01 20:59:40 -05:00
}
}
/**
* 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
* @constructor
*/
export const Inject = (key?: DependencyKey): 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)
2021-06-02 22:36:25 -05:00
if ( !key && type ) {
key = type
}
2021-06-01 20:59:40 -05:00
if ( key ) {
const existing = propertyMetadata.firstWhere('property', '=', property)
if ( existing ) {
existing.key = key
} else {
2021-06-02 22:36:25 -05:00
propertyMetadata.push({ property,
key })
2021-06-01 20:59:40 -05:00
}
}
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,
2021-06-02 22:36:25 -05:00
overridden: true,
2021-06-01 20:59:40 -05:00
})
}
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',
2021-06-02 22:36:25 -05:00
...(name ? { name } : {}),
2021-06-01 20:59:40 -05:00
}
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)
Injectable()(target)
if ( name ) {
ContainerBlueprint.getContainerBlueprint().registerNamed(name, target)
2021-06-01 20:59:40 -05:00
} else {
ContainerBlueprint.getContainerBlueprint().register(target)
2021-06-01 20:59:40 -05:00
}
}
}
}