diff --git a/.prettierignore b/.prettierignore index 1032f8a095d0..6863f7cc22dc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ packages/*/api-docs packages/cli/generators/*/templates package.json packages/*/package.json +*.md diff --git a/packages/authentication/src/decorators/authenticate.ts b/packages/authentication/src/decorators/authenticate.ts index 5a0a084608e0..75ce842bbd7c 100644 --- a/packages/authentication/src/decorators/authenticate.ts +++ b/packages/authentication/src/decorators/authenticate.ts @@ -3,7 +3,11 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Reflector, Constructor} from '@loopback/context'; +import { + MetadataInspector, + Constructor, + MethodDecoratorFactory, +} from '@loopback/context'; import {AuthenticationBindings} from '../keys'; /** @@ -21,18 +25,13 @@ export interface AuthenticationMetadata { * @param options Additional options to configure the authentication. */ export function authenticate(strategyName: string, options?: Object) { - return function(controllerClass: Object, methodName: string) { - const metadataObj: AuthenticationMetadata = { + return MethodDecoratorFactory.createDecorator( + AuthenticationBindings.METADATA, + { strategy: strategyName, options: options || {}, - }; - Reflector.defineMetadata( - AuthenticationBindings.METADATA, - metadataObj, - controllerClass, - methodName, - ); - }; + }, + ); } /** @@ -45,7 +44,7 @@ export function getAuthenticateMetadata( controllerClass: Constructor<{}>, methodName: string, ): AuthenticationMetadata | undefined { - return Reflector.getMetadata( + return MetadataInspector.getMethodMetadata( AuthenticationBindings.METADATA, controllerClass.prototype, methodName, diff --git a/packages/context/package.json b/packages/context/package.json index d9df5cf08169..cc85e4d4c8af 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -22,7 +22,7 @@ "author": "IBM", "license": "MIT", "dependencies": { - "reflect-metadata": "^0.1.10" + "@loopback/metadata": "^4.0.0-alpha.1" }, "devDependencies": { "@loopback/build": "^4.0.0-alpha.7", diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 45d0b27e945f..653089723952 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -14,7 +14,6 @@ export { export {Context} from './context'; export {Constructor} from './resolver'; export {inject, Setter, Getter} from './inject'; -export {NamespacedReflect} from './reflect'; export {Provider} from './provider'; export {isPromise} from './is-promise'; @@ -25,4 +24,5 @@ export { describeInjectedProperties, Injection, } from './inject'; -export {Reflector} from './reflect'; + +export * from '@loopback/metadata'; diff --git a/packages/context/src/inject.ts b/packages/context/src/inject.ts index b6c57006a10f..9ae4a48cd5cc 100644 --- a/packages/context/src/inject.ts +++ b/packages/context/src/inject.ts @@ -3,7 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Reflector} from './reflect'; +import { + MetadataInspector, + ParameterDecoratorFactory, + PropertyDecoratorFactory, + MetadataMap, +} from '@loopback/metadata'; import {BoundValue, ValueOrPromise} from './binding'; import {Context} from './context'; @@ -60,42 +65,51 @@ export function inject( return function markParameterOrPropertyAsInjected( // tslint:disable-next-line:no-any target: any, - propertyKey?: string | symbol, - propertyDescriptorOrParameterIndex?: + propertyKey: string | symbol, + methodDescriptorOrParameterIndex?: | TypedPropertyDescriptor | number, ) { - if (typeof propertyDescriptorOrParameterIndex === 'number') { + if (typeof methodDescriptorOrParameterIndex === 'number') { // The decorator is applied to a method parameter // Please note propertyKey is `undefined` for constructor - const injectedArgs: Injection[] = - Reflector.getOwnMetadata(PARAMETERS_KEY, target, propertyKey!) || []; - injectedArgs[propertyDescriptorOrParameterIndex] = { - bindingKey, - metadata, - resolve, - }; - Reflector.defineMetadata( + const paramDecorator: ParameterDecorator = ParameterDecoratorFactory.createDecorator( PARAMETERS_KEY, - injectedArgs, - target, - propertyKey!, + { + bindingKey, + metadata, + resolve, + }, ); + paramDecorator(target, propertyKey!, methodDescriptorOrParameterIndex); } else if (propertyKey) { + // Property or method if (typeof Object.getPrototypeOf(target) === 'function') { const prop = target.name + '.' + propertyKey.toString(); throw new Error( '@inject is not supported for a static property: ' + prop, ); } - // The decorator is applied to a property - const injections: {[p: string]: Injection} = - Reflector.getOwnMetadata(PROPERTIES_KEY, target) || {}; - injections[propertyKey] = {bindingKey, metadata, resolve}; - Reflector.defineMetadata(PROPERTIES_KEY, injections, target); + if (methodDescriptorOrParameterIndex) { + // Method + throw new Error( + '@inject cannot be used on a method: ' + propertyKey.toString(), + ); + } + const propDecorator: PropertyDecorator = PropertyDecoratorFactory.createDecorator( + PROPERTIES_KEY, + { + bindingKey, + metadata, + resolve, + }, + ); + propDecorator(target, propertyKey!); } else { + // It won't happen here as `@inject` is not compatible with ClassDecorator + /* istanbul ignore next */ throw new Error( - '@inject can only be used on properties or method parameters.', + '@inject can only be used on a property or a method parameter', ); } }; @@ -176,11 +190,13 @@ export function describeInjectedArguments( target: any, method?: string | symbol, ): Injection[] { - if (method) { - return Reflector.getMetadata(PARAMETERS_KEY, target, method) || []; - } else { - return Reflector.getMetadata(PARAMETERS_KEY, target) || []; - } + method = method || ''; + const meta = MetadataInspector.getAllParameterMetadata( + PARAMETERS_KEY, + target, + method, + ); + return meta || []; } /** @@ -191,22 +207,11 @@ export function describeInjectedArguments( export function describeInjectedProperties( // tslint:disable-next-line:no-any target: any, -): {[p: string]: Injection} { - const metadata: {[name: string]: Injection} = {}; - let obj = target; - while (true) { - const m = Reflector.getOwnMetadata(PROPERTIES_KEY, obj); - if (m) { - // Adding non-existent properties - for (const p in m) { - if (!(p in metadata)) { - metadata[p] = m[p]; - } - } - } - // Recurse into the prototype chain - obj = Object.getPrototypeOf(obj); - if (!obj) break; - } +): MetadataMap { + const metadata = + MetadataInspector.getAllPropertyMetadata( + PROPERTIES_KEY, + target, + ) || {}; return metadata; } diff --git a/packages/context/test/unit/inject.test.ts b/packages/context/test/unit/inject.test.ts index 104da2fa7635..cb486abb89a4 100644 --- a/packages/context/test/unit/inject.test.ts +++ b/packages/context/test/unit/inject.test.ts @@ -117,6 +117,16 @@ describe('property injection', () => { }).to.throw(/@inject is not supported for a static property/); }); + it('cannot decorate a method', () => { + expect(() => { + // tslint:disable-next-line:no-unused-variable + class TestClass { + @inject('bar') + foo() {} + } + }).to.throw(/@inject cannot be used on a method/); + }); + it('supports inheritance without overriding property', () => { class TestClass { @inject('foo') foo: string; diff --git a/packages/metadata/.gitignore b/packages/metadata/.gitignore new file mode 100644 index 000000000000..90a8d96cc3ff --- /dev/null +++ b/packages/metadata/.gitignore @@ -0,0 +1,3 @@ +*.tgz +dist* +package diff --git a/packages/metadata/.npmrc b/packages/metadata/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/metadata/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/metadata/LICENSE b/packages/metadata/LICENSE new file mode 100644 index 000000000000..35e4706e0582 --- /dev/null +++ b/packages/metadata/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017. All Rights Reserved. +Node module: @loopback/metadata +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/metadata/README.md b/packages/metadata/README.md new file mode 100644 index 000000000000..95f55ef65c24 --- /dev/null +++ b/packages/metadata/README.md @@ -0,0 +1,449 @@ +# @loopback/metadata + +This module contains utilities to help developers implement [TypeScript decorators](https://www.typescriptlang.org/docs/handbook/decorators.html), define/merge +metadata, and inspect metadata. + +* Reflector: Wrapper of + [reflect-metadata](https://github.com/rbuckton/reflect-metadata) +* Decorator factories: A set of factories for class/method/property/parameter + decorators to apply metadata to a given class and its static or instance + members. +* MetadataInspector: High level APIs to inspect a class and/or its members to + get metadata applied by decorators. + +# Usage + +## To create a class decorator + +```ts +import {ClassDecoratorFactory} from '@loopback/metadata'; + +export interface MyClassMetadata { + name: string; + description?: string; +} + +function myClassDecorator(spec: MyClassMetadata): ClassDecorator { + return ClassDecoratorFactory.createDecorator( + 'metadata-key-for-my-class-decorator', + spec, + ); +} +``` + +Alternativley, we can instantiate the factory and create a decorator: + +```ts +function myClassDecorator(spec: MyClassMetadata): ClassDecorator { + const factory = new ClassDecoratorFactory( + 'metadata-key-for-my-class-decorator', + spec, + ); + return factory.create(); +} +``` + +Now we can use `@myClassDecorator` to add metadata to a class as follows: + +```ts +@myClassDecorator({name: 'my-controller'}) +class MyController {} +``` + +## To create a method decorator + +```ts +import {MethodDecoratorFactory} from '@loopback/metadata'; + +export interface MyMethodMetadata { + name: string; + description?: string; +} + +function myMethodDecorator(spec: MyMethodMetadata): MethodDecorator { + return MethodDecoratorFactory.createDecorator( + 'metadata-key-for-my-method-decorator', + spec, + ); +} +``` + +Now we can use `@myMethodDecorator` to add metadata to a method as follows: + +```ts +class MyController { + @myMethodDecorator({name: 'my-method'}) + myMethod(x: string): string { + return 'Hello, ' + x; + } + + @myMethodDecorator({name: 'another-method'}) + anotherMethod() {} + + @myMethodDecorator({name: 'my-static-method'}) + static myStaticMethod() {} +} +``` + +## To create a property decorator + +```ts +import {PropertyDecoratorFactory} from '@loopback/metadata'; + +export interface MyPropertyMetadata { + name: string; + description?: string; +} + +function myPropertydDecorator(spec: MyPropertyMetadata): PropertyDecorator { + return PropertyDecoratorFactory.createDecorator( + 'metadata-key-for-my-property-decorator', + spec, + ); +} +``` + +Now we can use `@myPropertyDecorator` to add metadata to a property as follows: + +```ts +class MyController { + @myPropertyDecorator({name: 'my-property'}) + myProperty: string; + + @myPropertyDecorator({name: 'another-property'}) + anotherProperty: boolean; + + @myPropertyDecorator({name: 'my-static-property'}) + static myStaticProperty: string; +} +``` + +## To create a parameter decorator + +```ts +import {ParameterDecoratorFactory} from '@loopback/metadata'; + +export interface MyParameterMetadata { + name: string; + description?: string; +} + +function myParameterdDecorator(spec: MyParameterMetadata): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator( + 'metadata-key-for-my-parameter-decorator', + spec, + ); +} +``` + +Now we can use `@myParameterDecorator` to add metadata to a parameter as follows: + +```ts +class MyController { + constructor( + @myParameterDecorator({name: 'logging-prefix'}) + public prefix: string, + @myParameterDecorator({name: 'logging-level'}) + public level: number, + ) {} + + myMethod( + @myParameterDecorator({name: 'x'}) + x: number, + @myParameterDecorator({name: 'y'}) + y: number, + ) {} + + static myStaticMethod( + @myParameterDecorator({name: 'a'}) + a: string, + @myParameterDecorator({name: 'b'}) + b: string, + ) {} +} +``` + +## To create method decorator for parameters + +```ts +import {MethodParameterDecoratorFactory} from '@loopback/metadata'; + +export interface MyParameterMetadata { + name: string; + description?: string; +} + +function myMethodParameterDecorator( + spec: MyParameterMetadata, +): MethodDecorator { + return MethodParameterDecoratorFactory.createDecorator( + 'metadata-key-for-my-method-parameter-decorator', + spec, + ); +} +``` + +Now we can use `@myMethodParameterDecorator` to add metadata to a parameter +as follows: + +```ts +class MyController { + @myMethodParameterDecorator({name: 'x'}) + @myMethodParameterDecorator({name: 'y'}) + myMethod( + x: number, + y: number, + ) {} +``` + +**WARNING**: Using method decorators to provide metadata for parameters is +strongly discouraged for a few reasons: + +1. Method decorators cannot be applied to a constructor +2. Method decorators depends on the positions to match parameters + +We recommend that `ParameterDecorator` be used instead. + +## Customize inheritance of metadata + +By default, the decorator factories allow inheritance with the following rules: + +1. If the metadata is an object, we merge the `spec` argument from the decorator + function into the inherited value from base classes. For metadata of array and + other primitive types, the `spec` argument is used if provided. + + - We can override `inherit` method of the decorator factory to customize + how to resolve `spec` against the inherited metadata. For example: + + ```ts + protected inherit(inheritedMetadata: T | undefined | null): T { + // Ignore the inherited metadata + return this.spec; + } + ``` + +2. Method/property/parameter level metadata is applied to the class or its + prototype as a map keyed method/property names. We think this approach is better + than keeping metadata at method/property level as it's not easy to inspect a + class to find static/instance methods and properties with decorations. The + metadata for a class is illustrated below: + + - MyClass (the constructor function itself) + + ```ts + { + // Class level metadata + 'my-class-decorator-key': MyClassMetadata, + // Static method (including the construtor) parameter metadata + 'my-static-parameter-decorator-key': { + '': [MyConstructorParameterMetadata], // Constructor parameter metadata + 'myStaticMethod1': [MyStaticMethodParameterMetadata], + 'myStaticMethod2': [MyStaticMethodParameterMetadata], + }, + // Static method metadata + 'my-static-method-decorator-key': { + 'myStaticMethod1': MyStaticMethodMetadata, + 'myStaticMethod2': MyStaticMethodMetadata, + }, + // Static property metadata + 'my-static-property-decorator-key': { + 'myStaticMethod1': MyStaticPropertyMetadata, + 'myStaticMethod1': MyStaticPropertyMetadata, + } + } + ``` + + - MyClass.prototype + + ```ts + { + // Instance method parameter metadata + 'my-instance-parameter-decorator-key': { + 'myMethod1': [MyMethodParameterMetadata], + 'myMethod2': [MyMethodParameterMetadata], + }, + // Instance method metadata + 'my-instance-method-decorator-key': { + 'myMethod1': MyMethodMetadata, + 'myMethod2': MyMethodMetadata, + }, + // Instance property metadata + 'my-instance-property-decorator-key': { + 'myProperty1': MyPropertyMetadata, + 'myProperty2': MyPropertyMetadata, + } + } + ``` + + The following methods in `DecoratorFactory` allow subclasses to customize how + to merge the `spec` with existing metadata for a class, methods, properties, and + method parameters. Please note `M` is a map for methods/properties/parameters. + + ```ts + protected mergeWithInherited( + inheritedMetadata: M, + target: Object, + member?: string | symbol, + descriptorOrIndex?: TypedPropertyDescriptor | number, + ): M { + // ... + } + + protected mergeWithOwn( + ownMetadata: M, + target: Object, + member?: string | symbol, + descriptorOrIndex?: TypedPropertyDescriptor | number, + ): M { + // ... + } + ``` + +3. The default implemention throws errors if the same decorator function is applied +to a given target member (class/method/property/parameter) more than once. +For example, the following usage will report an error at runtime. + + ```ts + @myClassDecorator({name: 'my-controller'}) + @myClassDecorator({name: 'your-controller'}) + class MyController {} + ``` + +## Inspect metadata + +`MetadataInspector` provides API to inspect metadata from a class and its +members. + +## Inspect metadata of a class + +```ts +import {MetadataInspector} from '@loopback/metadata'; + +const meta = MetadataInspector.getClassMetadata( + 'my-class-decorator-key', + MyController, +); +``` + +## Inspect own metadata of a class + +```ts +import {MetadataInspector} from '@loopback/metadata'; + +const meta = MetadataInspector.getClassMetadata( + 'my-class-decorator-key', + MyController, + { + ownMetadataOnly: true, + }, +); +``` + +## Inspect metadata of a method + +```ts +import {MetadataInspector} from '@loopback/metadata'; + +const allMethods = MetadataInspector.getAllMethodMetaData( + 'my-method-decorator-key', + MyController.prototype, // Use MyController for static methods +); + +const myMethod = MetadataInspector.getMethodMetaData( + 'my-method-decorator-key', + MyController.prototype, // Use MyController for static methods + 'myMethod', +); +``` + +## Inspect metadata of a property + +```ts +import {MetadataInspector} from '@loopback/metadata'; + +const allProps = MetadataInspector.getAllPropertyMetaData( + 'my-property-decorator-key', + MyController.prototype, // Use MyController for static properties +); + +const myProp = MetadataInspector.getMethodMetaData( + 'my-property-decorator-key', + MyController.prototype, // Use MyController for static properties + 'myProp', +); +``` + +## Inspect metadata of method parameters + +```ts +import {MetadataInspector} from '@loopback/metadata'; + +const allParamsForMyMethod = + MetadataInspector.getAllParameterMetaData( + 'my-parameter-decorator-key', + MyController.prototype, // Use MyController for static methods, + 'myMethod', + ); + +const firstParamForMyMethod = + MetadataInspector.getMyParameterMetaData( + 'my-parameter-decorator-key', + MyController.prototype, // Use MyController for static methods + 'myMethod', + 0, // parameter index + ); + +const allParamsForConstructor = + MetadataInspector.getAllParameterMetaData( + 'my-parameter-decorator-key', + MyController, + '', + ); +``` + +## Inspect design-time metadata of properties/methods + +```ts +import {MetadataInspector} from '@loopback/metadata'; + +const myPropType = MetadataInspector.getDesignTypeForProperty( + MyController.prototype, + 'myProp', +); + +const myConstructor = MetadataInspector.getDesignTypeForMethod( + MyController, + '', +); + +const myMethod = MetadataInspector.getDesignTypeForMethod( + MyController.prototype, // Use MyController for static methods + 'myMethod', +); +``` + +## Installation + +``` +$ npm install --save @loopback/metadata +``` + +## Contributions + +IBM/StrongLoop is an active supporter of open source and welcomes contributions +to our projects as well as those of the Node.js community in general. For more +information on how to contribute please refer to the +[Contribution Guide](https://loopback.io/doc/en/contrib/index.html). + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/metadata/docs.json b/packages/metadata/docs.json new file mode 100644 index 000000000000..3072030cd637 --- /dev/null +++ b/packages/metadata/docs.json @@ -0,0 +1,13 @@ +{ + "content": [ + "index.ts", + "src/reflect.ts", + "src/decorator-factory.ts", + "src/index.ts" + ], + "codeSectionDepth": 4, + "assets": { + "/": "/docs", + "/docs": "/docs" + } +} diff --git a/packages/metadata/index.d.ts b/packages/metadata/index.d.ts new file mode 100644 index 000000000000..1369c4aa5461 --- /dev/null +++ b/packages/metadata/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/metadata +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist/src'; diff --git a/packages/metadata/index.js b/packages/metadata/index.js new file mode 100644 index 000000000000..fecbfa479272 --- /dev/null +++ b/packages/metadata/index.js @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/metadata +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const nodeMajorVersion = +process.versions.node.split('.')[0]; +module.exports = nodeMajorVersion >= 7 ? + require('./dist/src') : + require('./dist6/src'); diff --git a/packages/metadata/index.ts b/packages/metadata/index.ts new file mode 100644 index 000000000000..3e97b9e039e3 --- /dev/null +++ b/packages/metadata/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/metadata +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// NOTE(bajtos) This file is used by VSCode/TypeScriptServer at dev time only +export * from './src'; diff --git a/packages/metadata/package.json b/packages/metadata/package.json new file mode 100644 index 000000000000..fc0a47ff1867 --- /dev/null +++ b/packages/metadata/package.json @@ -0,0 +1,56 @@ +{ + "name": "@loopback/metadata", + "version": "4.0.0-alpha.1", + "description": "LoopBack's metadata utilities for reflection and decoration", + "engines": { + "node": ">=6" + }, + "scripts": { + "acceptance": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/acceptance/**/*.js'", + "build": "npm run build:dist && npm run build:dist6", + "build:current": "lb-tsc", + "build:dist": "lb-tsc es2017", + "build:dist6": "lb-tsc es2015", + "build:apidocs": "lb-apidocs", + "clean": "rm -rf loopback-metadata*.tgz dist* package", + "prepare": "npm run build && npm run build:apidocs", + "pretest": "npm run build:current", + "test": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/unit/**/*.js' 'DIST/test/acceptance/**/*.js'", + "unit": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/unit/**/*.js'", + "verify": "npm pack && tar xf loopback-metadata*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash": "^4.17.4", + "reflect-metadata": "^0.1.10" + }, + "devDependencies": { + "@loopback/build": "^4.0.0-alpha.6", + "@loopback/testlab": "^4.0.0-alpha.15", + "@types/debug": "0.0.30", + "@types/lodash": "^4.14.87" + }, + "keywords": [ + "LoopBack", + "Decorators", + "Reflect" + ], + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "dist6/src", + "api-docs", + "src" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/metadata/src/decorator-factory.ts b/packages/metadata/src/decorator-factory.ts new file mode 100644 index 000000000000..79c2aadbefbb --- /dev/null +++ b/packages/metadata/src/decorator-factory.ts @@ -0,0 +1,625 @@ +// Copyright IBM Corp. 2013,2017. 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 {Reflector} from './reflect'; +import * as _ from 'lodash'; +import * as debugModule from 'debug'; +const debug = debugModule('loopback:metadata:decorator'); + +// tslint:disable:no-any + +/** + * An object mapping keys to corresponding metadata + */ +export interface MetadataMap { + [propertyOrMethodName: string]: T; +} + +/** + * Options for a decorator + */ +export interface DecoratorOptions { + /** + * Inheritance will be honored + */ + allowsInheritance?: boolean; + [name: string]: any; +} + +/** + * Decorator function types + */ +export type DecoratorType = + | ClassDecorator + | PropertyDecorator + | MethodDecorator + | ParameterDecorator; + +/** + * 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('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 | MetadataMap, // Type of the metadata + D extends DecoratorType // Type of the decorator +> { + /** + * 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}, options); + } + + protected allowInheritance(): boolean { + return this.options && 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 == undefined) 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 name of a decoration target + * @param target Class or prototype of a class + * @param member Optional property/method name + * @param descriptorOrIndex Optional method descriptor or parameter index + */ + getTargetName( + target: Object, + member?: string | symbol, + descriptorOrIndex?: TypedPropertyDescriptor | number, + ) { + let name = + target instanceof Function + ? target.name + : target.constructor.name + '.prototype'; + if (member == null && descriptorOrIndex == null) { + return 'class ' + name; + } + if (member == null) member = 'constructor'; + if (typeof descriptorOrIndex === 'number') { + // Parameter + name = + 'parameter ' + + name + + '.' + + member.toString() + + '[' + + descriptorOrIndex + + ']'; + } else if (descriptorOrIndex != null) { + name = 'method ' + name + '.' + member.toString(); + } else { + name = 'property ' + name + '.' + member.toString(); + } + return name; + } + + /** + * Get the number of parameters for a given constructor or method + * @param target Class or the prototype + * @param member Method name + */ + getNumberOfParameters(target: Object, member?: string | symbol) { + if (target instanceof Function && member == null) { + // constructor + return target.length; + } else { + // target[member] is a function + return (<{[methodName: string]: Function}>target)[member!].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, + }); + } + 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 | number, + ): M { + throw new Error('mergeWithInherited() is not implemented'); + } + + /** + * 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 | number, + ): M { + throw new Error('mergeWithOwn() is not implemented'); + } + + /** + * Create a decorator function of the given type. Each sub class MUST + * implement this method. + */ + create(): D { + throw new Error('create() is not implemented'); + } + + /** + * 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 | number, + ) { + const targetName = this.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); + if (debug.enabled) { + debug('%s: %j', targetName, meta); + } + Reflector.defineMetadata(this.key, meta, target); + } else { + meta = this.mergeWithOwn(meta, target, member, descriptorOrIndex); + 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< + T, + M extends T | MetadataMap | MetadataMap, + D extends DecoratorType + >(key: string, spec: T, options?: DecoratorOptions): D { + const inst = new this(key, spec, options); + return inst.create(); + } + + static cloneDeep(val: T): T { + if (val === undefined) { + return {} as T; + } + return _.cloneDeepWith(val, v => { + // Do not clone functions + if (typeof v === 'function') return v; + return undefined; + }); + } +} + +/** + * Factory for class decorators + */ +export class ClassDecoratorFactory extends DecoratorFactory< + T, + T, + ClassDecorator +> { + protected mergeWithInherited( + inheritedMetadata: T, + target: Object, + member?: string | symbol, + descriptorOrIndex?: TypedPropertyDescriptor | number, + ) { + return this.withTarget(this.inherit(inheritedMetadata), target); + } + + protected mergeWithOwn( + ownMetadata: T, + target: Object, + member?: string | symbol, + descriptorOrIndex?: TypedPropertyDescriptor | number, + ) { + if (ownMetadata != null) { + throw new Error( + 'Decorator cannot be applied more than once on ' + + this.getTargetName(target), + ); + } + 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(key: string, spec: T, options?: DecoratorOptions) { + return super._createDecorator(key, spec, options); + } +} + +/** + * Factory for property decorators + */ +export class PropertyDecoratorFactory extends DecoratorFactory< + T, + MetadataMap, + PropertyDecorator +> { + protected mergeWithInherited( + inheritedMetadata: MetadataMap, + target: Object, + propertyName?: string | symbol, + descriptorOrIndex?: TypedPropertyDescriptor | number, + ) { + const propertyMeta: T = this.withTarget( + this.inherit(inheritedMetadata[propertyName!]), + target, + ); + inheritedMetadata[propertyName!] = propertyMeta; + return inheritedMetadata; + } + + protected mergeWithOwn( + ownMetadata: MetadataMap, + target: Object, + propertyName?: string | symbol, + descriptorOrParameterIndex?: TypedPropertyDescriptor | number, + ) { + if (ownMetadata == null) ownMetadata = {}; + if (ownMetadata[propertyName!] != null) { + const targetName = this.getTargetName(target, propertyName); + throw new Error( + 'Decorator cannot be applied more than once on ' + targetName, + ); + } + 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(key: string, spec: T, options?: DecoratorOptions) { + return super._createDecorator, PropertyDecorator>( + key, + spec, + options, + ); + } +} + +/** + * Factory for method decorators + */ +export class MethodDecoratorFactory extends DecoratorFactory< + T, + MetadataMap, + MethodDecorator +> { + protected mergeWithInherited( + inheritedMetadata: MetadataMap, + target: Object, + methodName?: string | symbol, + methodDescriptor?: TypedPropertyDescriptor | number, + ) { + const methodMeta = this.withTarget( + this.inherit(inheritedMetadata[methodName!]), + target, + ); + inheritedMetadata[methodName!] = methodMeta; + return inheritedMetadata; + } + + protected mergeWithOwn( + ownMetadata: MetadataMap, + target: Object, + methodName?: string | symbol, + methodDescriptor?: TypedPropertyDescriptor | number, + ) { + if (ownMetadata == null) ownMetadata = {}; + const methodMeta = ownMetadata[methodName!]; + if (this.getTarget(methodMeta) === target) { + throw new Error( + 'Decorator cannot be applied more than once on ' + + this.getTargetName(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, + ) => 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(key: string, spec: T, options?: DecoratorOptions) { + return super._createDecorator, MethodDecorator>( + key, + spec, + options, + ); + } +} + +/** + * Factory for parameter decorators + */ +export class ParameterDecoratorFactory extends DecoratorFactory< + T, + MetadataMap, + ParameterDecorator +> { + private getOrInitMetadata( + meta: MetadataMap, + target: Object, + methodName?: string | symbol, + ) { + const method = methodName ? methodName : ''; + let methodMeta = meta[method]; + if (methodMeta == null) { + // Initialize the method metadata + methodMeta = new Array( + this.getNumberOfParameters(target, methodName), + ).fill(undefined); + meta[method] = methodMeta; + } + return methodMeta; + } + + protected mergeWithInherited( + inheritedMetadata: MetadataMap, + target: Object, + methodName?: string | symbol, + parameterIndex?: TypedPropertyDescriptor | number, + ) { + const methodMeta = this.getOrInitMetadata( + inheritedMetadata, + target, + methodName, + ); + const index = parameterIndex as number; + methodMeta[index] = this.withTarget( + this.inherit(methodMeta[index]), + target, + ); + return inheritedMetadata; + } + + protected mergeWithOwn( + ownMetadata: MetadataMap, + target: Object, + methodName?: string | symbol, + parameterIndex?: TypedPropertyDescriptor | number, + ) { + if (ownMetadata == null) ownMetadata = {}; + // Find the method metadata + const methodMeta = this.getOrInitMetadata(ownMetadata, target, methodName); + const index = parameterIndex as number; + if (this.getTarget(methodMeta[index]) === target) { + throw new Error( + 'Decorator cannot be applied more than once on ' + + this.getTargetName(target, methodName, parameterIndex), + ); + } + // Set the parameter metadata + methodMeta[index] = this.withTarget( + this.inherit(methodMeta[index]), + target, + ); + return ownMetadata; + } + + create(): ParameterDecorator { + return ( + target: Object, + methodName: string | symbol, + 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(key: string, spec: T, options?: DecoratorOptions) { + return super._createDecorator, ParameterDecorator>( + key, + spec, + options, + ); + } +} + +/** + * Factory for method level parameter decorator. 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 extends DecoratorFactory< + T, + MetadataMap, + MethodDecorator +> { + protected mergeWithInherited( + inheritedMetadata: MetadataMap, + target: Object, + methodName?: string | symbol, + methodDescriptor?: TypedPropertyDescriptor | number, + ) { + return {[methodName!]: [this.spec]}; + } + + protected mergeWithOwn( + ownMetadata: MetadataMap, + target: Object, + methodName?: string | symbol, + methodDescriptor?: TypedPropertyDescriptor | number, + ) { + if (ownMetadata == null) ownMetadata = {}; + let params = ownMetadata[methodName!]; + params = [this.spec].concat(params); + ownMetadata[methodName!] = params; + return ownMetadata; + } + + create(): MethodDecorator { + return ( + target: Object, + methodName: string | symbol, + descriptor: TypedPropertyDescriptor, + ) => 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(key: string, spec: T, options?: DecoratorOptions) { + return super._createDecorator, MethodDecorator>( + key, + spec, + options, + ); + } +} diff --git a/packages/metadata/src/index.ts b/packages/metadata/src/index.ts new file mode 100644 index 000000000000..b5c4aee6c85e --- /dev/null +++ b/packages/metadata/src/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/metadata +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './reflect'; +export * from './decorator-factory'; +export * from './inspector'; diff --git a/packages/metadata/src/inspector.ts b/packages/metadata/src/inspector.ts new file mode 100644 index 000000000000..2afd191deb88 --- /dev/null +++ b/packages/metadata/src/inspector.ts @@ -0,0 +1,281 @@ +// Copyright IBM Corp. 2013,2017. 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 {Reflector, NamespacedReflect} from './reflect'; +import {MetadataMap} from './decorator-factory'; + +/** + * TypeScript reflector without a namespace. The TypeScript compiler can be + * configured to add design time metadata. + * + * See https://www.typescriptlang.org/docs/handbook/decorators.html + */ +const TSReflector = new NamespacedReflect(); + +/** + * Design time metadata for a method. + * + * @example + * ```ts + * class MyController + * { + * myMethod(x: string, y: number, z: MyClass): boolean { + * // ... + * return true; + * } + * } + * ``` + * + * The `myMethod` above has design-time metadata as follows: + * ```ts + * { + * type: Function, + * parameterTypes: [String, Number, MyClass], + * returnType: Boolean + * } + * ``` + */ +export interface DesignTimeMethodMetadata { + /** + * Type of the method itself. It is `Function` + */ + type: Function; + /** + * An array of parameter types + */ + parameterTypes: Function[]; + /** + * Return type + */ + returnType: Function; +} + +/** + * Options for inspection + */ +export interface InspectionOptions { + /** + * Only inspect own metadata of a given target. The prototype chain will not + * be checked. The implementation uses `Reflect.getOwnMetadata()` if the flag + * is set to `true`. Otherwise, it uses `Reflect.getMetadata()`. + * + * The flag is `false` by default for `MetadataInspector`. + */ + ownMetadataOnly?: boolean; +} + +/** + * Inspector for metadata applied by decorators + */ +export class MetadataInspector { + /** + * Expose Reflector, which is a wrapper of `Reflect` and it uses `loopback` + * as the namespace prefix for all metadata keys + */ + static readonly Reflector = Reflector; + /** + * Expose the reflector for TypeScript design-time metadata + */ + static readonly DesignTimeReflector = TSReflector; + + /** + * Get the metadata associated with the given key for a given class + * @param key Metadata key + * @param target Class that contains the metadata + * @param options Options for inspection + */ + static getClassMetadata( + key: string, + target: Function, + options?: InspectionOptions, + ): T | undefined { + return options && options.ownMetadataOnly + ? Reflector.getOwnMetadata(key, target) + : Reflector.getMetadata(key, target); + } + + /** + * Define metadata for the given target + * @param key Metadata key + * @param value Metadata value + * @param target Target for the metadata + * @param member Optional property or method name + */ + static defineMetadata( + key: string, + value: T, + target: Object, + member?: string | symbol, + ) { + Reflector.defineMetadata(key, value, target, member); + } + + /** + * Get the metadata associated with the given key for all methods of the + * target class or prototype + * @param key Metadata key + * @param target Class for static methods or prototype for instance methods + * @param options Options for inspection + */ + static getAllMethodMetadata( + key: string, + target: Object, + options?: InspectionOptions, + ): MetadataMap | undefined { + return options && options.ownMetadataOnly + ? Reflector.getOwnMetadata(key, target) + : Reflector.getMetadata(key, target); + } + + /** + * Get the metadata associated with the given key for a given method of the + * target class or prototype + * @param key Metadata key + * @param target Class for static methods or prototype for instance methods + * @param methodName Method name. If not present, default to '' to use + * the constructor + * @param options Options for inspection + */ + static getMethodMetadata( + key: string, + target: Object, + methodName?: string | symbol, + options?: InspectionOptions, + ): T | undefined { + methodName = methodName || ''; + const meta: MetadataMap = + options && options.ownMetadataOnly + ? Reflector.getOwnMetadata(key, target) + : Reflector.getMetadata(key, target); + return meta && meta[methodName]; + } + + /** + * Get the metadata associated with the given key for all properties of the + * target class or prototype + * @param key Metadata key + * @param target Class for static methods or prototype for instance methods + * @param options Options for inspection + */ + static getAllPropertyMetadata( + key: string, + target: Object, + options?: InspectionOptions, + ): MetadataMap | undefined { + return options && options.ownMetadataOnly + ? Reflector.getOwnMetadata(key, target) + : Reflector.getMetadata(key, target); + } + + /** + * Get the metadata associated with the given key for a given property of the + * target class or prototype + * @param key Metadata key + * @param target Class for static properties or prototype for instance + * properties + * @param propertyName Property name + * @param options Options for inspection + */ + static getPropertyMetadata( + key: string, + target: Object, + propertyName: string | symbol, + options?: InspectionOptions, + ): T | undefined { + const meta: MetadataMap = + options && options.ownMetadataOnly + ? Reflector.getOwnMetadata(key, target) + : Reflector.getMetadata(key, target); + return meta && meta[propertyName]; + } + + /** + * Get the metadata associated with the given key for all parameters of a + * given method + * @param key Metadata key + * @param target Class for static methods or prototype for instance methods + * @param methodName Method name. If not present, default to '' to use + * the constructor + * @param options Options for inspection + */ + static getAllParameterMetadata( + key: string, + target: Object, + methodName?: string | symbol, + options?: InspectionOptions, + ): T[] | undefined { + methodName = methodName || ''; + const meta: MetadataMap = + options && options.ownMetadataOnly + ? Reflector.getOwnMetadata(key, target) + : Reflector.getMetadata(key, target); + return meta && meta[methodName]; + } + + /** + * Get the metadata associated with the given key for a parameter of a given + * method by index + * @param key Metadata key + * @param target Class for static methods or prototype for instance methods + * @param methodName Method name. If not present, default to '' to use + * the constructor + * @param index Index of the parameter, starting with 0 + * @param options Options for inspection + */ + static getParameterMetadata( + key: string, + target: Object, + methodName: string | symbol, + index: number, + options?: InspectionOptions, + ): T | undefined { + methodName = methodName || ''; + const meta: MetadataMap = + options && options.ownMetadataOnly + ? Reflector.getOwnMetadata(key, target) + : Reflector.getMetadata(key, target); + const params = meta && meta[methodName]; + return params && params[index]; + } + + /** + * Get TypeScript design time type for a property + * @param target Class or prototype + * @param propertyName Property name + */ + static getDesignTypeForProperty( + target: Object, + propertyName: string | symbol, + ): Function { + return TSReflector.getMetadata('design:type', target, propertyName); + } + + /** + * Get TypeScript design time type for a method + * @param target Class or prototype + * @param methodName Method name + */ + static getDesignTypeForMethod( + target: Object, + methodName: string | symbol, + ): DesignTimeMethodMetadata { + const type = TSReflector.getMetadata('design:type', target, methodName); + const parameterTypes = TSReflector.getMetadata( + 'design:paramtypes', + target, + methodName, + ); + const returnType = TSReflector.getMetadata( + 'design:returntype', + target, + methodName, + ); + return { + type, + parameterTypes, + returnType, + }; + } +} diff --git a/packages/context/src/reflect.ts b/packages/metadata/src/reflect.ts similarity index 99% rename from packages/context/src/reflect.ts rename to packages/metadata/src/reflect.ts index aca65c9442f4..155225b4cedf 100644 --- a/packages/context/src/reflect.ts +++ b/packages/metadata/src/reflect.ts @@ -1,5 +1,5 @@ // Copyright IBM Corp. 2017. All Rights Reserved. -// Node module: @loopback/context +// Node module: @loopback/metadata // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT diff --git a/packages/metadata/test/unit/decorator-factory.test.ts b/packages/metadata/test/unit/decorator-factory.test.ts new file mode 100644 index 000000000000..2beb109dc0a6 --- /dev/null +++ b/packages/metadata/test/unit/decorator-factory.test.ts @@ -0,0 +1,551 @@ +// Copyright IBM Corp. 2013,2017. 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 {expect} from '@loopback/testlab'; +import { + ClassDecoratorFactory, + PropertyDecoratorFactory, + MethodDecoratorFactory, + ParameterDecoratorFactory, + DecoratorFactory, +} from '../..'; + +import {Reflector} from '../../src/reflect'; + +describe('ClassDecoratorFactory', () => { + /** + * Define `@classDecorator(spec)` + * @param spec + */ + function classDecorator(spec: object): ClassDecorator { + return ClassDecoratorFactory.createDecorator('test', spec); + } + + @classDecorator({x: 1}) + class BaseController {} + + @classDecorator({y: 2}) + class SubController extends BaseController {} + + it('applies metadata to a class', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + expect(meta[DecoratorFactory.TARGET]).to.equal(BaseController); + }); + + it('merges with base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta).to.eql({x: 1, y: 2}); + expect(meta[DecoratorFactory.TARGET]).to.equal(SubController); + }); + + it('does not mutate base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + expect(meta[DecoratorFactory.TARGET]).to.equal(BaseController); + }); + + it('throws if applied more than once on the target', () => { + expect(() => { + @classDecorator({x: 1}) + @classDecorator({y: 2}) + // tslint:disable-next-line:no-unused-variable + class MyController {} + }).to.throw( + /Decorator cannot be applied more than once on class MyController/, + ); + }); +}); + +describe('ClassDecoratorFactory with create', () => { + interface MySpec { + x?: number; + y?: number; + } + + /** + * Define `@classDecorator(spec)` + * @param spec + */ + function classDecorator(spec: MySpec): ClassDecorator { + const factory = new ClassDecoratorFactory('test', spec); + return factory.create(); + } + + @classDecorator({x: 1}) + class BaseController {} + + @classDecorator({y: 2}) + class SubController extends BaseController {} + + it('applies metadata to a class', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + }); + + it('merges with base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta).to.eql({x: 1, y: 2}); + }); + + it('does not mutate base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + }); +}); + +describe('ClassDecoratorFactory without inheritance', () => { + /** + * Define `@classDecorator(spec)` + * @param spec + */ + function classDecorator(spec: object): ClassDecorator { + return ClassDecoratorFactory.createDecorator('test', spec, { + allowInheritance: false, + }); + } + + @classDecorator({x: 1}) + class BaseController {} + + @classDecorator({y: 2}) + class SubController extends BaseController {} + + it('applies metadata to a class', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + }); + + it('merges with base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta).to.eql({y: 2}); + }); + + it('does not mutate base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + }); +}); + +describe('ClassDecoratorFactory does not inherit array values', () => { + /** + * Define `@classDecorator(spec)` + * @param spec + */ + function classDecorator(spec: object): ClassDecorator { + return ClassDecoratorFactory.createDecorator('test', spec); + } + + @classDecorator([1]) + class BaseController {} + + @classDecorator([2]) + class SubController extends BaseController {} + + it('applies metadata to a class', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql([1]); + }); + + it('merges with base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta).to.eql([2]); + }); + + it('does not mutate base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql([1]); + }); +}); + +describe('ClassDecoratorFactory with custom inherit', () => { + /** + * Define `@classDecorator(spec)` + * @param spec + */ + function classDecorator(spec: object): ClassDecorator { + class MyClassDecoratorFactory extends ClassDecoratorFactory { + /** + * Override the `inherit` method to skip metadata from the base + * @param baseMeta + */ + inherit(baseMeta: object) { + return this.spec; + } + } + return MyClassDecoratorFactory.createDecorator('test', spec); + } + + @classDecorator({x: 1}) + class BaseController {} + + @classDecorator({y: 2}) + class SubController extends BaseController {} + + it('applies metadata to a class', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + }); + + it('merges with base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta).to.eql({y: 2}); + }); + + it('does not mutate base class metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + }); +}); + +describe('PropertyDecoratorFactory', () => { + /** + * Define `@propertyDecorator(spec)` + * @param spec + */ + function propertyDecorator(spec: object): PropertyDecorator { + return PropertyDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @propertyDecorator({x: 1}) + myProp: string; + } + + class SubController extends BaseController { + @propertyDecorator({y: 2}) + myProp: string; + } + + it('applies metadata to a property', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myProp).to.eql({x: 1}); + }); + + it('merges with base property metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController.prototype); + expect(meta.myProp).to.eql({x: 1, y: 2}); + }); + + it('does not mutate base property metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myProp).to.eql({x: 1}); + }); + + it('throws if applied more than once on the same property', () => { + expect(() => { + // tslint:disable-next-line:no-unused-variable + class MyController { + @propertyDecorator({x: 1}) + @propertyDecorator({y: 2}) + myProp: string; + } + }).to.throw( + /Decorator cannot be applied more than once on property MyController\.prototype\.myProp/, + ); + }); +}); + +describe('PropertyDecoratorFactory for static properties', () => { + /** + * Define `@propertyDecorator(spec)` + * @param spec + */ + function propertyDecorator(spec: object): PropertyDecorator { + return PropertyDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @propertyDecorator({x: 1}) + static myProp: string; + } + + class SubController extends BaseController { + @propertyDecorator({y: 2}) + static myProp: string; + } + + it('applies metadata to a property', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.myProp).to.eql({x: 1}); + }); + + it('merges with base property metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta.myProp).to.eql({x: 1, y: 2}); + }); + + it('does not mutate base property metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.myProp).to.eql({x: 1}); + }); + + it('throws if applied more than once on the same static property', () => { + expect(() => { + // tslint:disable-next-line:no-unused-variable + class MyController { + @propertyDecorator({x: 1}) + @propertyDecorator({y: 2}) + static myProp: string; + } + }).to.throw( + /Decorator cannot be applied more than once on property MyController\.myProp/, + ); + }); +}); + +describe('MethodDecoratorFactory', () => { + /** + * Define `@methodDecorator(spec)` + * @param spec + */ + function methodDecorator(spec: object): MethodDecorator { + return MethodDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @methodDecorator({x: 1}) + myMethod() {} + } + + class SubController extends BaseController { + @methodDecorator({y: 2}) + myMethod() {} + } + + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myMethod).to.eql({x: 1}); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController.prototype); + expect(meta.myMethod).to.eql({x: 1, y: 2}); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myMethod).to.eql({x: 1}); + }); + + it('throws if applied more than once on the same method', () => { + expect(() => { + // tslint:disable-next-line:no-unused-variable + class MyController { + @methodDecorator({x: 1}) + @methodDecorator({y: 2}) + myMethod() {} + } + }).to.throw( + /Decorator cannot be applied more than once on method MyController\.prototype\.myMethod/, + ); + }); +}); + +describe('MethodDecoratorFactory for static methods', () => { + /** + * Define `@methodDecorator(spec)` + * @param spec + */ + function methodDecorator(spec: object): MethodDecorator { + return MethodDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @methodDecorator({x: 1}) + static myMethod() {} + } + + class SubController extends BaseController { + @methodDecorator({y: 2}) + static myMethod() {} + } + + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.myMethod).to.eql({x: 1}); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta.myMethod).to.eql({x: 1, y: 2}); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.myMethod).to.eql({x: 1}); + }); + + it('throws if applied more than once on the same static method', () => { + expect(() => { + // tslint:disable-next-line:no-unused-variable + class MyController { + @methodDecorator({x: 1}) + @methodDecorator({y: 2}) + static myMethod() {} + } + }).to.throw( + /Decorator cannot be applied more than once on method MyController\.myMethod/, + ); + }); +}); + +describe('ParameterDecoratorFactory', () => { + /** + * Define `@parameterDecorator(spec)` + * @param spec + */ + function parameterDecorator(spec: object): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + myMethod( + @parameterDecorator({x: 1}) + a: string, + b: number, + ) {} + } + + class SubController extends BaseController { + myMethod( + @parameterDecorator({y: 2}) + a: string, + @parameterDecorator({x: 2}) + b: number, + ) {} + } + + it('applies metadata to a method parameter', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myMethod).to.eql([{x: 1}, undefined]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController.prototype); + expect(meta.myMethod).to.eql([{x: 1, y: 2}, {x: 2}]); + }); + + it('does not mutate base method parameter metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myMethod).to.eql([{x: 1}, undefined]); + }); + + it('throws if applied more than once on the same parameter', () => { + expect(() => { + // tslint:disable-next-line:no-unused-variable + class MyController { + myMethod( + @parameterDecorator({x: 1}) + @parameterDecorator({y: 2}) + x: string, + ) {} + } + }).to.throw( + /Decorator cannot be applied more than once on parameter MyController\.prototype\.myMethod\[0\]/, + ); + }); +}); + +describe('ParameterDecoratorFactory for a constructor', () => { + /** + * Define `@parameterDecorator(spec)` + * @param spec + */ + function parameterDecorator(spec: object): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + constructor( + @parameterDecorator({x: 1}) + a: string, + b: number, + ) {} + } + + class SubController extends BaseController { + constructor( + @parameterDecorator({y: 2}) + a: string, + @parameterDecorator({x: 2}) + b: number, + ) { + super(a, b); + } + } + + it('applies metadata to a method parameter', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta['']).to.eql([{x: 1}, undefined]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta['']).to.eql([{x: 1, y: 2}, {x: 2}]); + }); + + it('does not mutate base method parameter metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta['']).to.eql([{x: 1}, undefined]); + }); +}); + +describe('ParameterDecoratorFactory for a static method', () => { + /** + * Define `@parameterDecorator(spec)` + * @param spec + */ + function parameterDecorator(spec: object): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + static myMethod( + @parameterDecorator({x: 1}) + a: string, + b: number, + ) {} + } + + class SubController extends BaseController { + static myMethod( + @parameterDecorator({y: 2}) + a: string, + @parameterDecorator({x: 2}) + b: number, + ) {} + } + + it('applies metadata to a method parameter', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta['myMethod']).to.eql([{x: 1}, undefined]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta['myMethod']).to.eql([{x: 1, y: 2}, {x: 2}]); + }); + + it('does not mutate base method parameter metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta['myMethod']).to.eql([{x: 1}, undefined]); + }); + + it('throws if applied more than once on the same parameter', () => { + expect(() => { + // tslint:disable-next-line:no-unused-variable + class MyController { + static myMethod( + @parameterDecorator({x: 1}) + @parameterDecorator({y: 2}) + x: string, + ) {} + } + }).to.throw( + /Decorator cannot be applied more than once on parameter MyController\.myMethod\[0\]/, + ); + }); +}); diff --git a/packages/metadata/test/unit/inspector.test.ts b/packages/metadata/test/unit/inspector.test.ts new file mode 100644 index 000000000000..e5785afa3c99 --- /dev/null +++ b/packages/metadata/test/unit/inspector.test.ts @@ -0,0 +1,662 @@ +// Copyright IBM Corp. 2013,2017. 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 {expect} from '@loopback/testlab'; +import { + ClassDecoratorFactory, + PropertyDecoratorFactory, + MethodDecoratorFactory, + ParameterDecoratorFactory, + MetadataInspector, +} from '../..'; + +describe('Inspector for a class', () => { + /** + * Define `@classDecorator(spec)` + * @param spec + */ + function classDecorator(spec: object): ClassDecorator { + return ClassDecoratorFactory.createDecorator('test', spec); + } + + @classDecorator({x: 1}) + class BaseController {} + + @classDecorator({y: 2}) + class SubController extends BaseController {} + + class AnotherController extends BaseController {} + + it('inspects metadata of a base class', () => { + const meta = MetadataInspector.getClassMetadata('test', BaseController); + expect(meta).to.eql({x: 1}); + }); + + it('inspects metadata of a sub class', () => { + const meta = MetadataInspector.getClassMetadata('test', SubController); + expect(meta).to.eql({x: 1, y: 2}); + }); + + it('inspects metadata of a sub class without override', () => { + const meta = MetadataInspector.getClassMetadata('test', AnotherController); + expect(meta).to.eql({x: 1}); + }); +}); + +describe('Inspector for a class for its own metadata', () => { + /** + * Define `@classDecorator(spec)` + * @param spec + */ + function classDecorator(spec: object): ClassDecorator { + return ClassDecoratorFactory.createDecorator('test', spec); + } + + @classDecorator({x: 1}) + class BaseController {} + + @classDecorator({y: 2}) + class SubController extends BaseController {} + + class AnotherController extends BaseController {} + + it('inspects metadata of a base class', () => { + const meta = MetadataInspector.getClassMetadata('test', BaseController, { + ownMetadataOnly: true, + }); + expect(meta).to.eql({x: 1}); + }); + + it('inspects metadata of a sub class', () => { + const meta = MetadataInspector.getClassMetadata('test', SubController, { + ownMetadataOnly: true, + }); + expect(meta).to.eql({x: 1, y: 2}); + }); + + it('inspects metadata of a sub class without override', () => { + const meta = MetadataInspector.getClassMetadata('test', AnotherController, { + ownMetadataOnly: true, + }); + expect(meta).to.be.undefined(); + }); +}); + +describe('Inspector for instance properties', () => { + /** + * Define `@propertyDecorator(spec)` + * @param spec + */ + function propertyDecorator(spec: object): PropertyDecorator { + return PropertyDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @propertyDecorator({x: 1}) + myProp: string; + } + + class SubController extends BaseController { + @propertyDecorator({y: 2}) + myProp: string; + } + + class AnotherController extends BaseController { + myProp: string; + } + + it('inspects metadata of all properties of a base class', () => { + const meta = MetadataInspector.getAllPropertyMetadata( + 'test', + BaseController.prototype, + ); + expect(meta).to.eql({myProp: {x: 1}}); + }); + + it('inspects metadata of a property of a base class', () => { + const meta = MetadataInspector.getPropertyMetadata( + 'test', + BaseController.prototype, + 'myProp', + ); + expect(meta).to.eql({x: 1}); + }); + + it('inspects metadata of all properties of a sub class', () => { + const meta = MetadataInspector.getAllPropertyMetadata( + 'test', + SubController.prototype, + ); + expect(meta).to.eql({myProp: {x: 1, y: 2}}); + }); + + it('inspects own metadata of all properties of a sub class', () => { + const meta = MetadataInspector.getAllPropertyMetadata( + 'test', + AnotherController.prototype, + {ownMetadataOnly: true}, + ); + expect(meta).to.be.undefined(); + + const propertyMeta = MetadataInspector.getPropertyMetadata( + 'test', + AnotherController.prototype, + 'myProp', + {ownMetadataOnly: true}, + ); + expect(propertyMeta).to.be.undefined(); + }); +}); + +describe('Inspector for static properties', () => { + /** + * Define `@propertyDecorator(spec)` + * @param spec + */ + function propertyDecorator(spec: object): PropertyDecorator { + return PropertyDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @propertyDecorator({x: 1}) + static myProp: string; + } + + class SubController extends BaseController { + @propertyDecorator({y: 2}) + static myProp: string; + } + + class AnotherController extends BaseController { + static myProp: string; + } + + it('inspects metadata of all properties of a base class', () => { + const meta = MetadataInspector.getAllPropertyMetadata( + 'test', + BaseController, + ); + expect(meta).to.eql({myProp: {x: 1}}); + }); + + it('inspects metadata of a property of a base class', () => { + const meta = MetadataInspector.getPropertyMetadata( + 'test', + BaseController, + 'myProp', + ); + expect(meta).to.eql({x: 1}); + }); + + it('inspects metadata of all properties of a sub class', () => { + const meta = MetadataInspector.getAllPropertyMetadata( + 'test', + SubController, + ); + expect(meta).to.eql({myProp: {x: 1, y: 2}}); + }); + + it('inspects own metadata of all properties of a sub class', () => { + const meta = MetadataInspector.getAllPropertyMetadata( + 'test', + AnotherController, + {ownMetadataOnly: true}, + ); + expect(meta).to.be.undefined(); + + const propertyMeta = MetadataInspector.getPropertyMetadata( + 'test', + AnotherController, + 'myProp', + {ownMetadataOnly: true}, + ); + expect(propertyMeta).to.be.undefined(); + }); +}); + +describe('Inspector for instance methods', () => { + /** + * Define `@methodDecorator(spec)` + * @param spec + */ + function methodDecorator(spec: object): MethodDecorator { + return MethodDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @methodDecorator({x: 1}) + myMethod() {} + } + + class SubController extends BaseController { + @methodDecorator({y: 2}) + myMethod() {} + } + + class AnotherController extends BaseController {} + + it('inspects metadata of all methods of a base class', () => { + const meta = MetadataInspector.getAllMethodMetadata( + 'test', + BaseController.prototype, + ); + expect(meta).to.eql({myMethod: {x: 1}}); + }); + + it('inspects metadata of a method of a base class', () => { + const meta = MetadataInspector.getMethodMetadata( + 'test', + BaseController.prototype, + 'myMethod', + ); + expect(meta).to.eql({x: 1}); + }); + + it('inspects metadata of all methods of a sub class', () => { + const meta = MetadataInspector.getAllMethodMetadata( + 'test', + SubController.prototype, + ); + expect(meta).to.eql({myMethod: {x: 1, y: 2}}); + }); + + it('inspects own metadata of all methods of a sub class', () => { + const meta = MetadataInspector.getAllMethodMetadata( + 'test', + AnotherController.prototype, + {ownMetadataOnly: true}, + ); + expect(meta).to.be.undefined(); + + const methodMeta = MetadataInspector.getMethodMetadata( + 'test', + AnotherController.prototype, + 'myMethod', + {ownMetadataOnly: true}, + ); + expect(methodMeta).to.be.undefined(); + }); +}); + +describe('Inspector for static methods', () => { + /** + * Define `@methodDecorator(spec)` + * @param spec + */ + function methodDecorator(spec: object): MethodDecorator { + return PropertyDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @methodDecorator({x: 1}) + static myMethod() {} + } + + class SubController extends BaseController { + @methodDecorator({y: 2}) + static myMethod() {} + } + + class AnotherController extends BaseController {} + + it('inspects metadata of all methods of a base class', () => { + const meta = MetadataInspector.getAllMethodMetadata('test', BaseController); + expect(meta).to.eql({myMethod: {x: 1}}); + }); + + it('inspects metadata of a property of a base class', () => { + const meta = MetadataInspector.getMethodMetadata( + 'test', + BaseController, + 'myMethod', + ); + expect(meta).to.eql({x: 1}); + }); + + it('inspects metadata of all properties of a sub class', () => { + const meta = MetadataInspector.getAllMethodMetadata('test', SubController); + expect(meta).to.eql({myMethod: {x: 1, y: 2}}); + }); + + it('inspects own metadata of all methods of a sub class', () => { + const meta = MetadataInspector.getAllMethodMetadata( + 'test', + AnotherController, + {ownMetadataOnly: true}, + ); + expect(meta).to.be.undefined(); + + const methodMeta = MetadataInspector.getMethodMetadata( + 'test', + AnotherController, + 'myMethod', + {ownMetadataOnly: true}, + ); + expect(methodMeta).to.be.undefined(); + + const inherited = MetadataInspector.getAllMethodMetadata( + 'test', + AnotherController, + ); + expect(inherited).to.eql({myMethod: {x: 1}}); + }); +}); + +describe('Inspector for parameters of an instance method', () => { + /** + * Define `@parameterDecorator(spec)` + * @param spec + */ + function parameterDecorator(spec: object): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + myMethod( + @parameterDecorator({x: 1}) + a: string, + b: number, + ) {} + } + + class SubController extends BaseController { + myMethod( + @parameterDecorator({y: 2}) + a: string, + @parameterDecorator({x: 2}) + b: number, + ) {} + } + + class AnotherController extends BaseController {} + + it('inspects metadata of all parameters of a method of the base class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + BaseController.prototype, + 'myMethod', + ); + expect(meta).to.eql([{x: 1}, undefined]); + }); + + it('inspects metadata of all parameters of a method of the sub class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + SubController.prototype, + 'myMethod', + ); + expect(meta).to.eql([{x: 1, y: 2}, {x: 2}]); + }); + + it('inspects metadata of a parameter of a method of the sub class', () => { + const meta = MetadataInspector.getParameterMetadata( + 'test', + SubController.prototype, + 'myMethod', + 0, + ); + expect(meta).to.eql({x: 1, y: 2}); + }); + + it('inspects own metadata of all method parameters of a sub class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + AnotherController.prototype, + 'myMethod', + {ownMetadataOnly: true}, + ); + expect(meta).to.be.undefined(); + + const paramsMeta = MetadataInspector.getParameterMetadata( + 'test', + AnotherController.prototype, + 'myMethod', + 0, + {ownMetadataOnly: true}, + ); + expect(paramsMeta).to.be.undefined(); + + const inherited = MetadataInspector.getAllMethodMetadata( + 'test', + AnotherController.prototype, + ); + expect(inherited).to.eql({myMethod: [{x: 1}, undefined]}); + }); +}); + +describe('Inspector for parameters of a static method', () => { + /** + * Define `@parameterDecorator(spec)` + * @param spec + */ + function parameterDecorator(spec: object): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + static myMethod( + @parameterDecorator({x: 1}) + a: string, + b: number, + ) {} + } + + class SubController extends BaseController { + static myMethod( + @parameterDecorator({y: 2}) + a: string, + @parameterDecorator({x: 2}) + b: number, + ) {} + } + + class AnotherController extends BaseController {} + + it('inspects metadata of all parameters of a method of the base class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + BaseController, + 'myMethod', + ); + expect(meta).to.eql([{x: 1}, undefined]); + }); + + it('inspects metadata of all parameters of a method of the sub class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + SubController, + 'myMethod', + ); + expect(meta).to.eql([{x: 1, y: 2}, {x: 2}]); + }); + + it('inspects metadata of a parameter of a method of the sub class', () => { + const meta = MetadataInspector.getParameterMetadata( + 'test', + SubController, + 'myMethod', + 0, + ); + expect(meta).to.eql({x: 1, y: 2}); + }); + + it('inspects own metadata of all method parameters of a sub class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + AnotherController, + 'myMethod', + {ownMetadataOnly: true}, + ); + expect(meta).to.be.undefined(); + + const paramsMeta = MetadataInspector.getParameterMetadata( + 'test', + AnotherController, + 'myMethod', + 0, + {ownMetadataOnly: true}, + ); + expect(paramsMeta).to.be.undefined(); + + const inherited = MetadataInspector.getAllMethodMetadata( + 'test', + AnotherController, + ); + expect(inherited).to.eql({myMethod: [{x: 1}, undefined]}); + }); +}); + +describe('Inspector for parameters of a constructor', () => { + /** + * Define `@parameterDecorator(spec)` + * @param spec + */ + function parameterDecorator(spec: object): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + constructor( + @parameterDecorator({x: 1}) + a: string, + b: number, + ) {} + } + + class SubController extends BaseController { + constructor( + @parameterDecorator({y: 2}) + a: string, + @parameterDecorator({x: 2}) + b: number, + ) { + super(a, b); + } + } + + it('inspects metadata of all parameters of the constructor of the base class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + BaseController, + ); + expect(meta).to.eql([{x: 1}, undefined]); + }); + + it('inspects metadata of all parameters of the constructor of the sub class', () => { + const meta = MetadataInspector.getAllParameterMetadata( + 'test', + SubController, + '', + ); + expect(meta).to.eql([{x: 1, y: 2}, {x: 2}]); + }); + + it('inspects metadata of a parameter of the constructor of the sub class', () => { + const meta = MetadataInspector.getParameterMetadata( + 'test', + SubController, + '', + 0, + ); + expect(meta).to.eql({x: 1, y: 2}); + }); +}); + +describe('Inspector for design time metadata', () => { + function propertyDecorator(spec?: object): PropertyDecorator { + return PropertyDecoratorFactory.createDecorator('test:properties', spec); + } + + function methodDecorator(spec?: object): MethodDecorator { + return MethodDecoratorFactory.createDecorator('test:methods', spec); + } + + function parameterDecorator(spec?: object): ParameterDecorator { + return ParameterDecoratorFactory.createDecorator('test:parameters', spec); + } + + class MyClass {} + + class MyController { + constructor( + @parameterDecorator({x: 1}) + a: string, + b: number, + ) {} + + @propertyDecorator() myProp: string; + + @propertyDecorator() myType: MyClass; + + @methodDecorator() + myMethod(x: string, y: number): boolean { + return false; + } + + @propertyDecorator() static myStaticProp = {}; + + @methodDecorator() + static myStaticMethod(x: string, y: number): boolean { + return false; + } + } + + it('inspects design time type for properties with simple type', () => { + const meta = MetadataInspector.getDesignTypeForProperty( + MyController.prototype, + 'myProp', + ); + expect(meta).to.eql(String); + }); + + it('inspects design time type for properties with class type', () => { + const meta = MetadataInspector.getDesignTypeForProperty( + MyController.prototype, + 'myType', + ); + expect(meta).to.eql(MyClass); + }); + + it('inspects design time type for static properties', () => { + const meta = MetadataInspector.getDesignTypeForProperty( + MyController, + 'myStaticProp', + ); + expect(meta).to.eql(Object); + }); + + it('inspects design time type for the constructor', () => { + const meta = MetadataInspector.getDesignTypeForMethod(MyController, ''); + expect(meta).to.eql({ + type: undefined, + returnType: undefined, + parameterTypes: [String, Number], + }); + }); + + it('inspects design time type for instance methods', () => { + const meta = MetadataInspector.getDesignTypeForMethod( + MyController.prototype, + 'myMethod', + ); + expect(meta).to.eql({ + type: Function, + returnType: Boolean, + parameterTypes: [String, Number], + }); + }); + + it('inspects design time type for static methods', () => { + const meta = MetadataInspector.getDesignTypeForMethod( + MyController, + 'myStaticMethod', + ); + expect(meta).to.eql({ + type: Function, + returnType: Boolean, + parameterTypes: [String, Number], + }); + }); +}); diff --git a/packages/context/test/unit/reflect.ts b/packages/metadata/test/unit/reflect.test.ts similarity index 99% rename from packages/context/test/unit/reflect.ts rename to packages/metadata/test/unit/reflect.test.ts index 59bad9e1d365..f2edd5793f67 100644 --- a/packages/context/test/unit/reflect.ts +++ b/packages/metadata/test/unit/reflect.test.ts @@ -1,10 +1,10 @@ // Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/context +// Node module: @loopback/metadata // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {NamespacedReflect, Reflector} from '../..'; +import {NamespacedReflect, Reflector} from '../../src/reflect'; import 'reflect-metadata'; function givenReflectContextWithNameSpace(): NamespacedReflect { diff --git a/packages/metadata/tsconfig.build.json b/packages/metadata/tsconfig.build.json new file mode 100644 index 000000000000..855e02848b35 --- /dev/null +++ b/packages/metadata/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["src", "test"] +} diff --git a/packages/repository/src/decorators/model.ts b/packages/repository/src/decorators/model.ts index 24644560858b..f25ca774d0e6 100644 --- a/packages/repository/src/decorators/model.ts +++ b/packages/repository/src/decorators/model.ts @@ -3,15 +3,19 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Reflector} from '@loopback/context'; +import { + MetadataInspector, + ClassDecoratorFactory, + PropertyDecoratorFactory, + MetadataMap, +} from '@loopback/context'; import {ModelDefinition, ModelDefinitionSyntax} from '../model'; import {PropertyDefinition} from '../index'; export const MODEL_KEY = 'loopback:model'; -export const PROPERTY_KEY = 'loopback:property'; export const MODEL_PROPERTIES_KEY = 'loopback:model-properties'; -type PropertyMap = {[name: string]: PropertyDefinition}; +type PropertyMap = MetadataMap; // tslint:disable:no-any @@ -21,24 +25,37 @@ type PropertyMap = {[name: string]: PropertyDefinition}; * @returns {(target:any)} */ export function model(definition?: ModelDefinitionSyntax) { - return function(target: any) { + return function(target: Function & {definition?: ModelDefinition}) { if (!definition) { definition = {name: target.name}; } - // Apply model definition to the model class - Reflector.defineMetadata(MODEL_KEY, definition, target); + const decorator = ClassDecoratorFactory.createDecorator( + MODEL_KEY, + definition, + ); + + decorator(target); // Build "ModelDefinition" and store it on model constructor const modelDef = new ModelDefinition(definition); - const propertyMap: PropertyMap = Reflector.getMetadata( - MODEL_PROPERTIES_KEY, - target.prototype, - ); + const propertyMap: PropertyMap = + MetadataInspector.getAllPropertyMetadata( + MODEL_PROPERTIES_KEY, + target.prototype, + ) || {}; for (const p in propertyMap) { - modelDef.addProperty(p, propertyMap[p]); + const propertyDef = propertyMap[p]; + const designType = MetadataInspector.getDesignTypeForProperty( + target.prototype, + p, + ); + if (!propertyDef.type) { + propertyDef.type = designType; + } + modelDef.addProperty(p, propertyDef); } target.definition = modelDef; @@ -50,19 +67,9 @@ export function model(definition?: ModelDefinitionSyntax) { * @param definition * @returns {(target:any, key:string)} */ -export function property(definition: PropertyDefinition) { - return function(target: any, key: string) { - // Apply model definition to the model class - Reflector.defineMetadata(PROPERTY_KEY, definition, target, key); - - // Because there is no way how to iterate decorated properties at runtime, - // we need to keep an explicit map of decorated properties - let map: PropertyMap = Reflector.getMetadata(MODEL_PROPERTIES_KEY, target); - if (!map) { - map = Object.create(null); - Reflector.defineMetadata(MODEL_PROPERTIES_KEY, map, target); - } - - map[key] = definition; - }; +export function property(definition: Partial) { + return PropertyDecoratorFactory.createDecorator( + MODEL_PROPERTIES_KEY, + definition, + ); } diff --git a/packages/repository/src/decorators/relation.ts b/packages/repository/src/decorators/relation.ts index 5999c443bfa0..ea1638e1f6ab 100644 --- a/packages/repository/src/decorators/relation.ts +++ b/packages/repository/src/decorators/relation.ts @@ -6,7 +6,7 @@ import {Class} from '../common-types'; import {Entity} from '../model'; -import {Reflector} from '@loopback/context'; +import {PropertyDecoratorFactory} from '@loopback/context'; // tslint:disable:no-any @@ -20,7 +20,7 @@ export enum RelationType { referencesMany, } -export const RELATION_KEY = 'loopback:relation'; +export const RELATIONS_KEY = 'loopback:relations'; export class RelationMetadata { type: RelationType; @@ -34,10 +34,8 @@ export class RelationMetadata { * @returns {(target:any, key:string)} */ export function relation(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - Reflector.defineMetadata(RELATION_KEY, definition, target, key); - }; + // Apply relation definition to the model class + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, definition); } /** @@ -46,11 +44,9 @@ export function relation(definition?: Object) { * @returns {(target:any, key:string)} */ export function belongsTo(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - const rel = Object.assign({type: RelationType.belongsTo}, definition); - Reflector.defineMetadata(RELATION_KEY, rel, target, key); - }; + // Apply model definition to the model class + const rel = Object.assign({type: RelationType.belongsTo}, definition); + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel); } /** @@ -59,11 +55,8 @@ export function belongsTo(definition?: Object) { * @returns {(target:any, key:string)} */ export function hasOne(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - const rel = Object.assign({type: RelationType.hasOne}, definition); - Reflector.defineMetadata(RELATION_KEY, rel, target, key); - }; + const rel = Object.assign({type: RelationType.hasOne}, definition); + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel); } /** @@ -72,11 +65,8 @@ export function hasOne(definition?: Object) { * @returns {(target:any, key:string)} */ export function hasMany(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - const rel = Object.assign({type: RelationType.hasMany}, definition); - Reflector.defineMetadata(RELATION_KEY, rel, target, key); - }; + const rel = Object.assign({type: RelationType.hasMany}, definition); + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel); } /** @@ -85,11 +75,8 @@ export function hasMany(definition?: Object) { * @returns {(target:any, key:string)} */ export function embedsOne(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - const rel = Object.assign({type: RelationType.embedsOne}, definition); - Reflector.defineMetadata(RELATION_KEY, rel, target, key); - }; + const rel = Object.assign({type: RelationType.embedsOne}, definition); + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel); } /** @@ -98,11 +85,8 @@ export function embedsOne(definition?: Object) { * @returns {(target:any, key:string)} */ export function embedsMany(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - const rel = Object.assign({type: RelationType.embedsMany}, definition); - Reflector.defineMetadata(RELATION_KEY, rel, target, key); - }; + const rel = Object.assign({type: RelationType.embedsMany}, definition); + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel); } /** @@ -111,11 +95,8 @@ export function embedsMany(definition?: Object) { * @returns {(target:any, key:string)} */ export function referencesOne(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - const rel = Object.assign({type: RelationType.referencesOne}, definition); - Reflector.defineMetadata(RELATION_KEY, rel, target, key); - }; + const rel = Object.assign({type: RelationType.referencesOne}, definition); + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel); } /** @@ -124,9 +105,6 @@ export function referencesOne(definition?: Object) { * @returns {(target:any, key:string)} */ export function referencesMany(definition?: Object) { - return function(target: any, key: string) { - // Apply model definition to the model class - const rel = Object.assign({type: RelationType.referencesMany}, definition); - Reflector.defineMetadata(RELATION_KEY, rel, target, key); - }; + const rel = Object.assign({type: RelationType.referencesMany}, definition); + return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel); } diff --git a/packages/repository/test/unit/decorator/model-and-relation.ts b/packages/repository/test/unit/decorator/model-and-relation.ts index 843a65da29de..3b1a05079cf7 100644 --- a/packages/repository/test/unit/decorator/model-and-relation.ts +++ b/packages/repository/test/unit/decorator/model-and-relation.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {model, property, MODEL_KEY, PROPERTY_KEY} from '../../../'; +import {model, property, MODEL_KEY, MODEL_PROPERTIES_KEY} from '../../../'; import { relation, hasOne, @@ -14,12 +14,12 @@ import { hasMany, referencesMany, referencesOne, - RELATION_KEY, + RELATIONS_KEY, RelationType, } from '../../../'; import {Entity, ValueObject} from '../../../'; -import {Reflector} from '@loopback/context'; +import {MetadataInspector} from '@loopback/context'; describe('model decorator', () => { @model() @@ -54,7 +54,6 @@ describe('model decorator', () => { @model({name: 'order'}) class Order extends Entity { @property({ - type: 'number', mysql: { column: 'QTY', }, @@ -96,109 +95,110 @@ describe('model decorator', () => { // Skip the tests before we resolve the issue around global `Reflector` // The tests are passing it run alone but fails with `npm test` it('adds model metadata', () => { - const meta = Reflector.getOwnMetadata(MODEL_KEY, Order); + const meta = MetadataInspector.getClassMetadata(MODEL_KEY, Order); expect(meta).to.eql({name: 'order'}); }); it('adds property metadata', () => { - const meta = Reflector.getOwnMetadata( - PROPERTY_KEY, - Order.prototype, - 'quantity', - ); - expect(meta).to.eql({ - type: 'number', + const meta = + MetadataInspector.getAllPropertyMetadata( + MODEL_PROPERTIES_KEY, + Order.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.quantity).to.eql({ + type: Number, mysql: { column: 'QTY', }, }); + expect(meta.id).to.eql({type: 'string', id: true, generated: true}); }); it('adds embedsOne metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Customer.prototype, - 'address', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Customer.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.address).to.eql({ type: RelationType.embedsOne, }); }); it('adds embedsMany metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Customer.prototype, - 'phones', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Customer.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.phones).to.eql({ type: RelationType.embedsMany, }); }); it('adds referencesMany metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Customer.prototype, - 'accounts', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Customer.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.accounts).to.eql({ type: RelationType.referencesMany, }); }); it('adds referencesOne metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Customer.prototype, - 'profile', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Customer.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.profile).to.eql({ type: RelationType.referencesOne, }); }); it('adds hasMany metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Customer.prototype, - 'orders', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Customer.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.orders).to.eql({ type: RelationType.hasMany, }); }); it('adds belongsTo metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Order.prototype, - 'customer', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Order.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.customer).to.eql({ type: RelationType.belongsTo, target: 'Customer', }); }); it('adds hasOne metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Customer.prototype, - 'lastOrder', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Customer.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.lastOrder).to.eql({ type: RelationType.hasOne, }); }); it('adds relation metadata', () => { - const meta = Reflector.getOwnMetadata( - RELATION_KEY, - Customer.prototype, - 'recentOrders', - ); - expect(meta).to.eql({ + const meta = + MetadataInspector.getAllPropertyMetadata( + RELATIONS_KEY, + Customer.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.recentOrders).to.eql({ type: RelationType.hasMany, }); }); diff --git a/packages/rest/src/router/metadata.ts b/packages/rest/src/router/metadata.ts index 912afbda4502..1996ae423ac4 100644 --- a/packages/rest/src/router/metadata.ts +++ b/packages/rest/src/router/metadata.ts @@ -3,10 +3,15 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import * as assert from 'assert'; -import * as _ from 'lodash'; +import { + MetadataInspector, + ClassDecoratorFactory, + MethodDecoratorFactory, + ParameterDecoratorFactory, + DecoratorFactory, + MethodParameterDecoratorFactory, +} from '@loopback/context'; -import {Reflector} from '@loopback/context'; import { OperationObject, ParameterLocation, @@ -16,24 +21,16 @@ import { PathsObject, } from '@loopback/openapi-spec'; -const debug = require('debug')('loopback:core:router:metadata'); +const debug = require('debug')('loopback:rest:router:metadata'); -const ENDPOINTS_KEY = 'rest:endpoints'; -const API_SPEC_KEY = 'rest:api-spec'; +const REST_METHODS_KEY = 'rest:methods'; +const REST_METHODS_WITH_PARAMETERS_KEY = 'rest:methods:parameters'; +const REST_PARAMETERS_KEY = 'rest:parameters'; +const REST_CLASS_KEY = 'rest:class'; +const REST_API_SPEC_KEY = 'rest:api-spec'; // tslint:disable:no-any -function cloneDeep(val: T): T { - if (val === undefined) { - return {} as T; - } - return _.cloneDeepWith(val, v => { - // Do not clone functions - if (typeof v === 'function') return v; - return undefined; - }); -} - export interface ControllerSpec { /** * The base path on which the Controller API is served. @@ -66,14 +63,10 @@ export interface ControllerSpec { * @decorator */ export function api(spec: ControllerSpec) { - return function(constructor: Function) { - assert( - typeof constructor === 'function', - 'The @api decorator can be applied to constructors only.', - ); - const apiSpec = resolveControllerSpec(constructor, spec); - Reflector.defineMetadata(API_SPEC_KEY, apiSpec, constructor); - }; + return ClassDecoratorFactory.createDecorator( + REST_CLASS_KEY, + spec, + ); } /** @@ -83,54 +76,44 @@ interface RestEndpoint { verb: string; path: string; spec?: OperationObject; - target: any; -} - -function getEndpoints( - target: any, -): {[property: string]: Partial} { - let endpoints = Reflector.getOwnMetadata(ENDPOINTS_KEY, target); - if (!endpoints) { - // Clone the endpoints so that subclasses won't mutate the metadata - // in the base class - const baseEndpoints = Reflector.getMetadata(ENDPOINTS_KEY, target); - endpoints = cloneDeep(baseEndpoints); - Reflector.defineMetadata(ENDPOINTS_KEY, endpoints, target); - } - return endpoints; } /** * Build the api spec from class and method level decorations * @param constructor Controller class - * @param spec API spec */ -function resolveControllerSpec( - constructor: Function, - spec?: ControllerSpec, -): ControllerSpec { +function resolveControllerSpec(constructor: Function): ControllerSpec { debug(`Retrieving OpenAPI specification for controller ${constructor.name}`); + let spec = MetadataInspector.getClassMetadata( + REST_CLASS_KEY, + constructor, + ); if (spec) { debug(' using class-level spec defined via @api()', spec); - spec = cloneDeep(spec); + spec = DecoratorFactory.cloneDeep(spec); } else { spec = {paths: {}}; } - const endpoints = getEndpoints(constructor.prototype); + let endpoints = + MetadataInspector.getAllMethodMetadata( + REST_METHODS_KEY, + constructor.prototype, + ) || {}; + endpoints = DecoratorFactory.cloneDeep(endpoints); for (const op in endpoints) { + debug(' processing method %s', op); + const endpoint = endpoints[op]; const verb = endpoint.verb!; const path = endpoint.path!; let endpointName = ''; + /* istanbul ignore if */ if (debug.enabled) { - const className = - endpoint.target.constructor.name || - constructor.name || - ''; + const className = constructor.name || ''; const fullMethodName = `${className}.${op}`; endpointName = `${fullMethodName} (${verb} ${path})`; } @@ -143,7 +126,26 @@ function resolveControllerSpec( }; endpoint.spec = operationSpec; } + debug(' operation for method %s: %j', op, endpoint); + debug(' processing parameters for method %s', op); + let params = MetadataInspector.getAllParameterMetadata( + REST_PARAMETERS_KEY, + constructor.prototype, + op, + ); + if (params == null) { + params = MetadataInspector.getMethodMetadata( + REST_METHODS_WITH_PARAMETERS_KEY, + constructor.prototype, + op, + ); + } + debug(' parameters for method %s: %j', op, params); + if (params != null) { + params = DecoratorFactory.cloneDeep(params); + operationSpec.parameters = params; + } operationSpec['x-operation-name'] = op; if (!spec.paths[path]) { @@ -166,10 +168,14 @@ function resolveControllerSpec( * @param constructor Controller class */ export function getControllerSpec(constructor: Function): ControllerSpec { - let spec = Reflector.getOwnMetadata(API_SPEC_KEY, constructor); + let spec = MetadataInspector.getClassMetadata( + REST_API_SPEC_KEY, + constructor, + {ownMetadataOnly: true}, + ); if (!spec) { - spec = resolveControllerSpec(constructor, spec); - Reflector.defineMetadata(API_SPEC_KEY, spec, constructor); + spec = resolveControllerSpec(constructor); + MetadataInspector.defineMetadata(REST_API_SPEC_KEY, spec, constructor); } return spec; } @@ -243,53 +249,14 @@ export function del(path: string, spec?: OperationObject) { * of this operation. */ export function operation(verb: string, path: string, spec?: OperationObject) { - return function( - target: any, - propertyKey: string, - descriptor: PropertyDescriptor, - ) { - assert( - typeof target[propertyKey] === 'function', - '@operation decorator can be applied to methods only', - ); - - const endpoints = getEndpoints(target); - let endpoint = endpoints[propertyKey]; - if (!endpoint || endpoint.target !== target) { - // Add the new endpoint metadata for the method - endpoint = {verb, path, spec, target}; - endpoints[propertyKey] = endpoint; - } else { - // Update the endpoint metadata - // It can be created by @param - endpoint.verb = verb; - endpoint.path = path; - endpoint.target = target; - } - - if (!spec) { - // Users can define parameters and responses using decorators - return; - } - - // Decorator are invoked in reverse order of their definition. - // For example, a method decorated with @operation() @param() - // will invoke param() decorator first and operation() second. - // As a result, we need to preserve any partial definitions - // already provided by other decorators. - editOperationSpec(endpoint, overrides => { - const mergedSpec = Object.assign({}, spec, overrides); - - // Merge "responses" definitions - mergedSpec.responses = Object.assign( - {}, - spec.responses, - overrides.responses, - ); - - return mergedSpec; - }); - }; + return MethodDecoratorFactory.createDecorator>( + REST_METHODS_KEY, + { + verb, + path, + spec, + }, + ); } const paramDecoratorStyle = Symbol('ParamDecoratorStyle'); @@ -322,71 +289,45 @@ const paramDecoratorStyle = Symbol('ParamDecoratorStyle'); */ export function param(paramSpec: ParameterObject) { return function( - target: any, - propertyKey: string, - descriptorOrParameterIndex: PropertyDescriptor | number, + target: Object, + member: string | symbol, + descriptorOrIndex: TypedPropertyDescriptor | number, ) { - assert( - typeof target[propertyKey] === 'function', - '@param decorator can be applied to methods only', - ); - - const endpoints = getEndpoints(target); - let endpoint = endpoints[propertyKey]; - if (!endpoint || endpoint.target !== target) { - const baseEndpoint = endpoint; - // Add the new endpoint metadata for the method - endpoint = cloneDeep(baseEndpoint); - endpoint.target = target; - endpoints[propertyKey] = endpoint; - } - - editOperationSpec(endpoint, operationSpec => { - let decoratorStyle; - if (typeof descriptorOrParameterIndex === 'number') { - decoratorStyle = 'parameter'; - } else { - decoratorStyle = 'method'; + const targetWithParamStyle = target as any; + if (typeof descriptorOrIndex === 'number') { + if (targetWithParamStyle[paramDecoratorStyle] === 'method') { + // This should not happen as parameter decorators are applied before + // the method decorator + /* istanbul ignore next */ + throw new Error( + 'Mixed usage of @param at method/parameter level' + + ' is not allowed.', + ); } - if (!operationSpec.parameters) { - operationSpec.parameters = []; - // Record the @param decorator style to ensure consistency - operationSpec[paramDecoratorStyle] = decoratorStyle; - } else { - // Mixed usage of @param at method/parameter level is not allowed - if (operationSpec[paramDecoratorStyle] !== decoratorStyle) { - throw new Error( - 'Mixed usage of @param at method/parameter level' + - ' is not allowed.', - ); - } - } - - if (typeof descriptorOrParameterIndex === 'number') { - operationSpec.parameters[descriptorOrParameterIndex] = paramSpec; - } else { - operationSpec.parameters.unshift(paramSpec); + targetWithParamStyle[paramDecoratorStyle] = 'parameter'; + ParameterDecoratorFactory.createDecorator( + REST_PARAMETERS_KEY, + paramSpec, + )(target, member, descriptorOrIndex); + } else { + if (targetWithParamStyle[paramDecoratorStyle] === 'parameter') { + throw new Error( + 'Mixed usage of @param at method/parameter level' + + ' is not allowed.', + ); } - - return operationSpec; - }); + targetWithParamStyle[paramDecoratorStyle] = 'method'; + RestMethodParameterDecoratorFactory.createDecorator( + REST_METHODS_WITH_PARAMETERS_KEY, + paramSpec, + )(target, member, descriptorOrIndex); + } }; } -function editOperationSpec( - endpoint: Partial, - updateFn: (spec: OperationObject) => OperationObject, -) { - let spec = endpoint.spec; - if (!spec) { - spec = { - responses: {}, - }; - } - - spec = updateFn(spec); - endpoint.spec = spec; -} +class RestMethodParameterDecoratorFactory extends MethodParameterDecoratorFactory< + ParameterObject +> {} export namespace param { export const query = { diff --git a/packages/rest/test/unit/router/metadata.test.ts b/packages/rest/test/unit/router/metadata.test.ts index c00653602e08..c7ac4dc164fc 100644 --- a/packages/rest/test/unit/router/metadata.test.ts +++ b/packages/rest/test/unit/router/metadata.test.ts @@ -34,6 +34,23 @@ describe('Routing metadata', () => { expect(actualSpec).to.eql(expectedSpec); }); + it('caches controller spec', () => { + const expectedSpec = anOpenApiSpec() + .withOperationReturningString('get', '/greet', 'greet') + .build(); + + @api(expectedSpec) + class MyController { + greet() { + return 'Hello world!'; + } + } + + const spec1 = getControllerSpec(MyController); + const spec2 = getControllerSpec(MyController); + expect(spec2).to.be.exactly(spec1); + }); + it('returns spec defined via @get decorator', () => { const operationSpec = anOperationSpec() .withStringResponse()