@loopback/metadata
Version:
Utilities to help developers implement TypeScript decorators, define/merge metadata, and inspect metadata
873 lines (818 loc) • 24.9 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
// Node module: @loopback/metadata
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import debugModule from 'debug';
import _ from 'lodash';
import {Reflector} from './reflect';
import {DecoratorType, MetadataKey, MetadataMap} from './types';
const debug = debugModule('loopback:metadata:decorator');
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Options for a decorator
*/
export interface DecoratorOptions {
/**
* Controls if inherited metadata will be honored. Default to `true`.
*/
allowInheritance?: boolean;
/**
* Controls if the value of `spec` argument will be cloned. Sometimes we
* use shared spec for the decoration, but the decorator function might need
* to mutate the object. Cloning the input spec makes it safe to use the same
* spec (`template`) to decorate different members.
*
* Default to `true`.
*/
cloneInputSpec?: boolean;
/**
* Name of the decorator for debugging purpose, such as `@inject`
*/
decoratorName?: string;
[name: string]: any;
}
/**
* Base factory class for decorator functions
*
* @example
* ```
* function classDecorator(spec: MySpec): ClassDecorator {
* return ClassDecoratorFactory.createDecorator('my-key', spec);
* }
* ```
* or
* ```
* function classDecorator(spec: MySpec): ClassDecorator {
* const factory: ClassDecoratorFactory<MySpec>('my-key', spec);
* return factory.create();
* }
* ```
* These functions above declare `@classDecorator` that can be used as follows:
* ```
* @classDecorator({x: 1})
* class MyController {}
* ```
*/
export class DecoratorFactory<
T, // Type of the metadata spec for individual class/method/property/parameter
M extends T | MetadataMap<T> | MetadataMap<T[]>, // Type of the metadata
D extends DecoratorType, // Type of the decorator
> {
protected decoratorName: string;
/**
* A constant to reference the target of a decoration
*/
static TARGET = '__decoratorTarget';
/**
* Construct a new class decorator factory
* @param key - Metadata key
* @param spec - Metadata object from the decorator function
* @param options - Options for the decorator. Default to
* `{allowInheritance: true}` if not provided
*/
constructor(
protected key: string,
protected spec: T,
protected options: DecoratorOptions = {},
) {
this.options = Object.assign(
{
allowInheritance: true,
cloneInputSpec: true,
},
options,
);
const defaultDecoratorName = this.constructor.name.replace(/Factory$/, '');
this.decoratorName = this.options.decoratorName ?? defaultDecoratorName;
if (this.options.cloneInputSpec) {
this.spec = DecoratorFactory.cloneDeep(spec);
}
}
protected allowInheritance(): boolean {
return !!this.options?.allowInheritance;
}
/**
* Inherit metadata from base classes. By default, this method merges base
* metadata into the spec if `allowInheritance` is set to `true`. To customize
* the behavior, this method can be overridden by sub classes.
*
* @param inheritedMetadata - Metadata from base classes for the member
*/
protected inherit(inheritedMetadata: T | undefined | null): T {
if (!this.allowInheritance()) return this.spec;
if (inheritedMetadata == null) return this.spec;
if (this.spec == null) return inheritedMetadata;
if (typeof inheritedMetadata !== 'object') return this.spec;
if (Array.isArray(inheritedMetadata) || Array.isArray(this.spec)) {
// For arrays, we don't merge
return this.spec;
}
return Object.assign(inheritedMetadata, this.spec);
}
/**
* Get the qualified name of a decoration target.
*
* @remarks
*
* Example of target names:
*
* - class MyClass
* - MyClass.constructor[0] // First parameter of the constructor
* - MyClass.myStaticProperty
* - MyClass.myStaticMethod()
* - MyClass.myStaticMethod[0] // First parameter of the myStaticMethod
* - MyClass.prototype.myProperty
* - MyClass.prototype.myMethod()
* - MyClass.prototype.myMethod[1] // Second parameter of myMethod
*
* @param target - Class or prototype of a class
* @param member - Optional property/method name
* @param descriptorOrIndex - Optional method descriptor or parameter index
*/
static getTargetName(
target: Object,
member?: string | symbol,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
) {
let name =
target instanceof Function
? target.name
: `${target.constructor.name}.prototype`;
if (member == null && descriptorOrIndex == null) {
return `class ${name}`;
}
if (member == null || member === '') member = 'constructor';
const memberAccessor =
typeof member === 'symbol' ? '[' + member.toString() + ']' : '.' + member;
if (typeof descriptorOrIndex === 'number') {
// Parameter
name = `${name}${memberAccessor}[${descriptorOrIndex}]`;
} else if (descriptorOrIndex != null) {
name = `${name}${memberAccessor}()`;
} else {
name = `${name}${memberAccessor}`;
}
return name;
}
/**
* Get the number of parameters for a given constructor or method
* @param target - Class or the prototype
* @param member - Method name
*/
static getNumberOfParameters(target: Object, member?: string) {
if (typeof target === 'function' && !member) {
// constructor
return target.length;
} else {
// target[member] is a function
const method = (<{[methodName: string]: Function}>target)[member!];
return method.length;
}
}
/**
* Set a reference to the target class or prototype for a given spec if
* it's an object
* @param spec - Metadata spec
* @param target - Target of the decoration. It is a class or the prototype of
* a class.
*/
withTarget(spec: T, target: Object) {
if (typeof spec === 'object' && spec != null) {
// Add a hidden property for the `target`
Object.defineProperty(spec, DecoratorFactory.TARGET, {
value: target,
enumerable: false,
// Make sure it won't be redefined on the same object
configurable: false,
});
}
return spec;
}
/**
* Get the optional decoration target of a given spec
* @param spec - Metadata spec
*/
getTarget(spec: T) {
if (typeof spec === 'object' && spec != null) {
const specWithTarget = spec as {[name: string]: any};
return specWithTarget[DecoratorFactory.TARGET];
} else {
return undefined;
}
}
/**
* This method is called by the default implementation of the decorator
* function to merge the spec argument from the decoration with the inherited
* metadata for a class, all properties, all methods, or all method
* parameters that are decorated by this decorator.
*
* It MUST be overridden by subclasses to process inherited metadata.
*
* @param inheritedMetadata - Metadata inherited from the base classes
* @param target - Decoration target
* @param member - Optional property or method
* @param descriptorOrIndex - Optional parameter index or method descriptor
*/
protected mergeWithInherited(
inheritedMetadata: M,
target: Object,
member?: string | symbol,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
): M {
throw new Error(
`mergeWithInherited() is not implemented for ${this.decoratorName}`,
);
}
/**
* This method is called by the default implementation of the decorator
* function to merge the spec argument from the decoration with the own
* metadata for a class, all properties, all methods, or all method
* parameters that are decorated by this decorator.
*
* It MUST be overridden by subclasses to process own metadata.
*
* @param ownMetadata - Own Metadata exists locally on the target
* @param target - Decoration target
* @param member - Optional property or method
* @param descriptorOrIndex - Optional parameter index or method descriptor
*/
protected mergeWithOwn(
ownMetadata: M,
target: Object,
member?: string | symbol,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
): M {
throw new Error(
`mergeWithOwn() is not implemented for ${this.decoratorName}`,
);
}
/**
* Create an error to report if the decorator is applied to the target more
* than once
* @param target - Decoration target
* @param member - Optional property or method
* @param descriptorOrIndex - Optional parameter index or method descriptor
*/
protected duplicateDecorationError(
target: Object,
member?: string | symbol,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
) {
const targetName = DecoratorFactory.getTargetName(
target,
member,
descriptorOrIndex,
);
return new Error(
`${this.decoratorName} cannot be applied more than once on ${targetName}`,
);
}
/**
* Create a decorator function of the given type. Each sub class MUST
* implement this method.
*/
create(): D {
throw new Error(`create() is not implemented for ${this.decoratorName}`);
}
/**
* Base implementation of the decorator function
* @param target - Decorator target
* @param member - Optional property or method
* @param descriptorOrIndex - Optional method descriptor or parameter index
*/
protected decorate(
target: Object,
member?: string | symbol,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
) {
const targetName = DecoratorFactory.getTargetName(
target,
member,
descriptorOrIndex,
);
let meta: M = Reflector.getOwnMetadata(this.key, target);
if (meta == null && this.allowInheritance()) {
// Clone the base metadata so that it won't be accidentally
// mutated by sub classes
meta = DecoratorFactory.cloneDeep(
Reflector.getMetadata(this.key, target),
);
meta = this.mergeWithInherited(meta, target, member, descriptorOrIndex);
/* istanbul ignore if */
if (debug.enabled) {
debug('%s: %j', targetName, meta);
}
Reflector.defineMetadata(this.key, meta, target);
} else {
meta = this.mergeWithOwn(meta, target, member, descriptorOrIndex);
/* istanbul ignore if */
if (debug.enabled) {
debug('%s: %j', targetName, meta);
}
Reflector.defineMetadata(this.key, meta, target);
}
}
/**
* Create a decorator function
* @param key - Metadata key
* @param spec - Metadata object from the decorator function
* @param options - Options for the decorator
*/
protected static _createDecorator<
S,
MT extends S | MetadataMap<S> | MetadataMap<S[]>,
DT extends DecoratorType,
>(key: MetadataKey<S, DT>, spec: S, options?: DecoratorOptions): DT {
const inst = new this<S, MT, DT>(key.toString(), spec, options);
return inst.create();
}
// See https://github.com/lodash/lodash/blob/master/.internal/baseClone.js
private static _cloneableTypes = [
Object,
Array,
Set,
Map,
RegExp,
Date,
Buffer,
ArrayBuffer,
Float32Array,
Float64Array,
Int8Array,
Int16Array,
Int32Array,
Uint8Array,
Uint8ClampedArray,
Uint16Array,
Uint32Array,
];
static cloneDeep<V>(val: Readonly<V>): V {
if (typeof val !== 'object') return val;
return _.cloneDeepWith(val, v => {
if (typeof v !== 'object') return v;
if (v == null) return v;
if (
v.constructor != null &&
!DecoratorFactory._cloneableTypes.includes(v.constructor)
) {
// Do not clone instances of classes/constructors, such as Date
return v;
}
return undefined;
});
}
}
/**
* Factory for class decorators
*/
export class ClassDecoratorFactory<T> extends DecoratorFactory<
T,
T,
ClassDecorator
> {
protected mergeWithInherited(
inheritedMetadata: T,
target: Object,
member?: string,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
) {
return this.withTarget(<T>this.inherit(inheritedMetadata), target);
}
protected mergeWithOwn(
ownMetadata: T,
target: Object,
member?: string,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
) {
if (ownMetadata != null) {
throw this.duplicateDecorationError(target, member, descriptorOrIndex);
}
return this.withTarget(this.spec, target);
}
create(): ClassDecorator {
return (target: Function) => this.decorate(target);
}
/**
* Create a class decorator function
* @param key - Metadata key
* @param spec - Metadata object from the decorator function
* @param options - Options for the decorator
*/
static createDecorator<S>(
key: MetadataKey<S, ClassDecorator>,
spec: S,
options?: DecoratorOptions,
) {
return super._createDecorator<S, S, ClassDecorator>(key, spec, options);
}
}
/**
* Factory for property decorators
*/
export class PropertyDecoratorFactory<T> extends DecoratorFactory<
T,
MetadataMap<T>,
PropertyDecorator
> {
protected mergeWithInherited(
inheritedMetadata: MetadataMap<T>,
target: Object,
propertyName?: string,
descriptorOrIndex?: TypedPropertyDescriptor<any> | number,
) {
inheritedMetadata = inheritedMetadata || {};
const propertyMeta: T = this.withTarget(
<T>this.inherit(inheritedMetadata[propertyName!]),
target,
);
inheritedMetadata[propertyName!] = propertyMeta;
return inheritedMetadata;
}
protected mergeWithOwn(
ownMetadata: MetadataMap<T>,
target: Object,
propertyName?: string,
descriptorOrParameterIndex?: TypedPropertyDescriptor<any> | number,
) {
ownMetadata = ownMetadata || {};
if (ownMetadata[propertyName!] != null) {
throw this.duplicateDecorationError(
target,
propertyName,
descriptorOrParameterIndex,
);
}
ownMetadata[propertyName!] = this.withTarget(this.spec, target);
return ownMetadata;
}
create(): PropertyDecorator {
return (target: Object, propertyName: string | symbol) =>
this.decorate(target, propertyName);
}
/**
* Create a property decorator function
* @param key - Metadata key
* @param spec - Metadata object from the decorator function
* @param options - Options for the decorator
*/
static createDecorator<S>(
key: MetadataKey<S, PropertyDecorator>,
spec: S,
options?: DecoratorOptions,
) {
return super._createDecorator<S, MetadataMap<S>, PropertyDecorator>(
key,
spec,
options,
);
}
}
/**
* Factory for method decorators
*/
export class MethodDecoratorFactory<T> extends DecoratorFactory<
T,
MetadataMap<T>,
MethodDecorator
> {
protected mergeWithInherited(
inheritedMetadata: MetadataMap<T>,
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
inheritedMetadata = inheritedMetadata || {};
const methodMeta = this.withTarget(
<T>this.inherit(inheritedMetadata[methodName!]),
target,
);
inheritedMetadata[methodName!] = methodMeta;
return inheritedMetadata;
}
protected mergeWithOwn(
ownMetadata: MetadataMap<T>,
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
ownMetadata = ownMetadata || {};
const methodMeta = ownMetadata[methodName!];
if (this.getTarget(methodMeta) === target) {
throw this.duplicateDecorationError(target, methodName, methodDescriptor);
}
// Set the method metadata
ownMetadata[methodName!] = this.withTarget(this.spec, target);
return ownMetadata;
}
create(): MethodDecorator {
return (
target: Object,
methodName: string | symbol,
descriptor: TypedPropertyDescriptor<any>,
) => this.decorate(target, methodName, descriptor);
}
/**
* Create a method decorator function
* @param key - Metadata key
* @param spec - Metadata object from the decorator function
* @param options - Options for the decorator
*/
static createDecorator<S>(
key: MetadataKey<S, MethodDecorator>,
spec: S,
options?: DecoratorOptions,
) {
return super._createDecorator<S, MetadataMap<S>, MethodDecorator>(
key,
spec,
options,
);
}
}
/**
* Factory for parameter decorators
*/
export class ParameterDecoratorFactory<T> extends DecoratorFactory<
T,
MetadataMap<T[]>,
ParameterDecorator
> {
private getOrInitMetadata(
meta: MetadataMap<T[]>,
target: Object,
methodName?: string,
) {
const method = methodName ? methodName : '';
let methodMeta = meta[method];
if (methodMeta == null) {
// Initialize the method metadata
methodMeta = new Array(
DecoratorFactory.getNumberOfParameters(target, methodName),
).fill(undefined);
meta[method] = methodMeta;
}
return methodMeta;
}
protected mergeWithInherited(
inheritedMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
parameterIndex?: TypedPropertyDescriptor<any> | number,
) {
inheritedMetadata = inheritedMetadata || {};
const methodMeta = this.getOrInitMetadata(
inheritedMetadata,
target,
methodName,
);
const index = parameterIndex as number;
methodMeta[index] = this.withTarget(
<T>this.inherit(methodMeta[index]),
target,
);
return inheritedMetadata;
}
protected mergeWithOwn(
ownMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
parameterIndex?: TypedPropertyDescriptor<any> | number,
) {
ownMetadata = ownMetadata || {};
// Find the method metadata
const methodMeta = this.getOrInitMetadata(ownMetadata, target, methodName);
const index = parameterIndex as number;
if (this.getTarget(methodMeta[index]) === target) {
throw this.duplicateDecorationError(target, methodName, parameterIndex);
}
// Set the parameter metadata
methodMeta[index] = this.withTarget(
<T>this.inherit(methodMeta[index]),
target,
);
return ownMetadata;
}
create(): ParameterDecorator {
return (
target: Object,
methodName: string | symbol | undefined,
parameterIndex: number,
) => this.decorate(target, methodName, parameterIndex);
}
/**
* Create a parameter decorator function
* @param key - Metadata key
* @param spec - Metadata object from the decorator function
* @param options - Options for the decorator
*/
static createDecorator<S>(
key: MetadataKey<S, ParameterDecorator>,
spec: S,
options?: DecoratorOptions,
) {
return super._createDecorator<S, MetadataMap<S[]>, ParameterDecorator>(
key,
spec,
options,
);
}
}
/**
* Factory for method level parameter decorator.
*
* @example
* For example, the following code uses `@param` to declare two parameters for
* `greet()`.
* ```ts
* class MyController {
* @param('name') // Parameter 0
* @param('msg') // Parameter 1
* greet() {}
* }
* ```
*/
export class MethodParameterDecoratorFactory<T> extends DecoratorFactory<
T,
MetadataMap<T[]>,
MethodDecorator
> {
/**
* Find the corresponding parameter index for the decoration
* @param target
* @param methodName
* @param methodDescriptor
*/
private getParameterIndex(
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
const numOfParams = DecoratorFactory.getNumberOfParameters(
target,
methodName,
);
// Fetch the cached parameter index
let index = Reflector.getOwnMetadata(
`${this.key}:index`,
target,
methodName,
);
// Default to the last parameter
if (index == null) index = numOfParams - 1;
if (index < 0) {
// Excessive decorations than the number of parameters detected
const method = DecoratorFactory.getTargetName(
target,
methodName,
methodDescriptor,
);
throw new Error(
`${this.decoratorName} is used more than ${numOfParams} time(s) on ${method}`,
);
}
return index;
}
protected mergeWithInherited(
inheritedMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
inheritedMetadata = inheritedMetadata || {};
const index = this.getParameterIndex(target, methodName, methodDescriptor);
const inheritedParams =
inheritedMetadata[methodName!] || new Array(index + 1).fill(undefined);
if (inheritedParams.length) {
// First time applied to a method. This is the last parameter of the method
inheritedParams[index] = this.withTarget(
<T>this.inherit(inheritedParams[index]),
target,
);
}
// Cache the index to help us position the next parameter
Reflector.defineMetadata(
`${this.key}:index`,
index - 1,
target,
methodName,
);
inheritedMetadata[methodName!] = inheritedParams;
return inheritedMetadata;
}
protected mergeWithOwn(
ownMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
ownMetadata = ownMetadata || {};
const index = this.getParameterIndex(target, methodName, methodDescriptor);
const params =
ownMetadata[methodName!] || new Array(index + 1).fill(undefined);
params[index] = this.withTarget(<T>this.inherit(params[index]), target);
ownMetadata[methodName!] = params;
// Cache the index to help us position the next parameter
Reflector.defineMetadata(
`${this.key}:index`,
index - 1,
target,
methodName,
);
return ownMetadata;
}
create(): MethodDecorator {
return (
target: Object,
methodName: string | symbol,
descriptor: TypedPropertyDescriptor<any>,
) => this.decorate(target, methodName, descriptor);
}
/**
* Create a method decorator function
* @param key - Metadata key
* @param spec - Metadata object from the decorator function
* @param options - Options for the decorator
*/
static createDecorator<S>(
key: MetadataKey<S, MethodDecorator>,
spec: S,
options?: DecoratorOptions,
) {
return super._createDecorator<S, MetadataMap<S[]>, MethodDecorator>(
key,
spec,
options,
);
}
}
/**
* Factory for an append-array of method-level decorators
* The `@response` metadata for a method is an array.
* Each item in the array should be a single value, containing
* a response code and a single spec or Model. This should allow:
*
* @example
* ```ts
* @response(200, MyFirstModel)
* @response(403, [NotAuthorizedReasonOne, NotAuthorizedReasonTwo])
* @response(404, NotFoundOne)
* @response(404, NotFoundTwo)
* @response(409, {schema: {}})
* public async myMethod() {}
* ```
*
* In the case that a ResponseObject is passed, it becomes the
* default for description/content, and if possible, further Models are
* incorporated as a `oneOf: []` array.
*
* In the case that a ReferenceObject is passed, it and it alone is used, since
* references can be external and we cannot `oneOf` their content.
*
* The factory creates and updates an array of items T[], and the getter
* provides the values as that array.
*/
export class MethodMultiDecoratorFactory<T> extends MethodDecoratorFactory<
T[]
> {
protected mergeWithInherited(
inheritedMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
) {
inheritedMetadata = inheritedMetadata || {};
inheritedMetadata[methodName!] = this._mergeArray(
inheritedMetadata[methodName!],
this.withTarget(this.spec, target),
);
return inheritedMetadata;
}
protected mergeWithOwn(
ownMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
ownMetadata = ownMetadata || {};
ownMetadata[methodName!] = this._mergeArray(
ownMetadata[methodName!],
this.withTarget(this.spec, target),
);
return ownMetadata;
}
private _mergeArray(result: T[], methodMeta: T | T[]) {
if (!result) {
if (Array.isArray(methodMeta)) {
result = methodMeta;
} else {
result = [methodMeta];
}
} else {
if (Array.isArray(methodMeta)) {
result.push(...methodMeta);
} else {
result.push(methodMeta);
}
}
return result;
}
}