diff --git a/packages/metadata/README.md b/packages/metadata/README.md index e1537be5b6dd..5b939745f2dc 100644 --- a/packages/metadata/README.md +++ b/packages/metadata/README.md @@ -87,6 +87,114 @@ class MyController { } ``` +### To create a decorator that can be used multiple times on a single method + +Instead of a single immutable object to be merged, the +`MethodMultiDecoratorFactory` reduced parameters into a flat array of items. +When fetching the metadata later, you will receive it as an array. + +```ts +import {MethodMultiDecoratorFactory} from '@loopback/metadata'; + +function myMultiMethodDecorator(spec: object): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator( + 'metadata-key-for-my-method-multi-decorator', + spec, + ); +} +``` + +Now, you can use it multiple times on a method: + +```ts +class MyController { + @myMultiMethodDecorator({x: 1}) + @myMultiMethodDecorator({y: 2}) + @myMultiMethodDecorator({z: 3}) + public point() {} +} + +class MyOtherController { + @myMultiMethodDecorator([{x: 1}, {y: 2}, {z: 3}]) + public point() {} +} +``` + +And when you access this data: + +```ts +const arrayOfSpecs = MetadataInspector.getMethodMetadata( + 'metadata-key-for-my-method-multi-decorator', + constructor.prototype, + op, +); + +// [{z: 3}, {y: 2}, {x: 1}] +``` + +Typescript +[applies decorators in reverse order](https://www.typescriptlang.org/docs/handbook/decorators.html) +per class, from the parent down. The metadata array resurned by `getOwnMetadata` +will be in this order: + +```ts +class Parent { + @myMultiMethodDecorator('A') // second + @myMultiMethodDecorator('B') // first + public greet() {} +} + +class Child extends Parent { + @myMultiMethodDecorator(['C', 'D']) // [third, fourth] + public greet() {} +} + +class Grandchild extends Child { + @myMultiMethodDecorator('E') // sixth + @myMultiMethodDecorator('F') // fifth + public greet() {} +} +// getMethodMetadata = ['B', 'A', 'C', 'D', 'F', 'E'] +``` + +You can also create a decorator that takes an object that can contain an array: + +```ts +interface Point { + x?: number; + y?: number; + z?: number; +} +interface GeometryMetadata { + points: Point[]; +} +function geometry(...points: Point[]): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator( + 'metadata-key-for-my-method-multi-decorator', + points, + ); +} + +class MyGeoController { + @geometry({x: 1}) + @geometry({x: 2}, {y: 3}) + @geometry({z: 5}) + public abstract() {} +} + +const arrayOfSpecs = MetadataInspector.getMethodMetadata( + 'metadata-key-for-my-method-multi-decorator', + constructor.prototype, + op, +); + +// [ +// { points: [{x: 1}]}, +// { points: [{x:2}, {y:3}]}, +// { points: [{z: 5}]}, +// ] +``` + ### To create a property decorator ```ts diff --git a/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts b/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts index d942761399eb..14c613f03596 100644 --- a/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts +++ b/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts @@ -8,6 +8,7 @@ import { ClassDecoratorFactory, DecoratorFactory, MethodDecoratorFactory, + MethodMultiDecoratorFactory, MethodParameterDecoratorFactory, ParameterDecoratorFactory, PropertyDecoratorFactory, @@ -523,6 +524,165 @@ describe('MethodDecoratorFactory for static methods', () => { }); }); +describe('MethodMultiDecoratorFactory', () => { + function methodMultiArrayDecorator(...spec: object[]): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } + + function methodMultiDecorator(spec: object): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @methodMultiArrayDecorator({x: 1}) + public myMethod() {} + + @methodMultiArrayDecorator({foo: 1}) + @methodMultiArrayDecorator({foo: 2}) + @methodMultiArrayDecorator({foo: 3}, {foo: 4}) + public multiMethod() {} + + @methodMultiDecorator({a: 'a'}) + @methodMultiDecorator({b: 'b'}) + public checkDecorator() {} + } + + class SubController extends BaseController { + @methodMultiArrayDecorator({y: 2}) + public myMethod() {} + + @methodMultiArrayDecorator({bar: 1}) + @methodMultiArrayDecorator({bar: 2}, {bar: 3}) + public multiMethod() {} + } + + describe('single-decorator methods', () => { + 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}]); + }); + }); + + describe('multi-decorator methods', () => { + it('applies to non-array decorator creation', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.checkDecorator).to.eql([{b: 'b'}, {a: 'a'}]); + }); + + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.multiMethod).to.eql([{foo: 3}, {foo: 4}, {foo: 2}, {foo: 1}]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController.prototype); + expect(meta.multiMethod).to.eql([ + {foo: 3}, + {foo: 4}, + {foo: 2}, + {foo: 1}, + {bar: 2}, + {bar: 3}, + {bar: 1}, + ]); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.multiMethod).to.eql([{foo: 3}, {foo: 4}, {foo: 2}, {foo: 1}]); + }); + }); +}); +describe('MethodMultiDecoratorFactory for static methods', () => { + function methodMultiArrayDecorator(...spec: object[]): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } + + function methodMultiDecorator(spec: object): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } + + class BaseController { + @methodMultiArrayDecorator({x: 1}) + static myMethod() {} + + @methodMultiArrayDecorator({foo: 1}) + @methodMultiArrayDecorator({foo: 2}) + @methodMultiArrayDecorator({foo: 3}, {foo: 4}) + static multiMethod() {} + + @methodMultiDecorator({a: 'a'}) + @methodMultiDecorator({b: 'b'}) + static checkDecorator() {} + } + + class SubController extends BaseController { + @methodMultiArrayDecorator({y: 2}) + static myMethod() {} + + @methodMultiArrayDecorator({bar: 1}) + @methodMultiArrayDecorator({bar: 2}, {bar: 3}) + static multiMethod() {} + } + + describe('single-decorator methods', () => { + 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}]); + }); + }); + + describe('multi-decorator methods', () => { + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.multiMethod).to.eql([{foo: 3}, {foo: 4}, {foo: 2}, {foo: 1}]); + }); + + it('applies to non-array decorator creation', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.checkDecorator).to.eql([{b: 'b'}, {a: 'a'}]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta.multiMethod).to.eql([ + {foo: 3}, + {foo: 4}, + {foo: 2}, + {foo: 1}, + {bar: 2}, + {bar: 3}, + {bar: 1}, + ]); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.multiMethod).to.eql([{foo: 3}, {foo: 4}, {foo: 2}, {foo: 1}]); + }); + }); +}); + describe('ParameterDecoratorFactory', () => { /** * Define `@parameterDecorator(spec)` diff --git a/packages/metadata/src/decorator-factory.ts b/packages/metadata/src/decorator-factory.ts index 3c8a10dbbc14..10888c621812 100644 --- a/packages/metadata/src/decorator-factory.ts +++ b/packages/metadata/src/decorator-factory.ts @@ -789,3 +789,78 @@ export class MethodParameterDecoratorFactory extends DecoratorFactory< ); } } + +/** + * Factory for an append-array of method-level decorators + * The @response metadata for a method is an array. + * Each item in the array should be a single value, containing + * a response code and a single spec or Model. This should allow: + * ```ts + * @response(200, MyFirstModel) + * @response(403, [NotAuthorizedReasonOne, NotAuthorizedReasonTwo]) + * @response(404, NotFoundOne) + * @response(404, NotFoundTwo) + * @response(409, {schema: {}}) + * public async myMethod() {} + * ``` + * + * In the case that a ResponseObject is passed, it becomes the + * default for description/content, and if possible, further Models are + * incorporated as a `oneOf: []` array. + * + * In the case that a ReferenceObject is passed, it and it alone is used, since + * references can be external and we cannot `oneOf` their content. + * + * The factory creates and updates an array of items T[], and the getter + * provides the values as that array. + */ +export class MethodMultiDecoratorFactory extends MethodDecoratorFactory< + T[] +> { + protected mergeWithInherited( + inheritedMetadata: MetadataMap, + target: Object, + methodName?: string, + ) { + inheritedMetadata = inheritedMetadata || {}; + + inheritedMetadata[methodName!] = this._mergeArray( + inheritedMetadata[methodName!], + this.withTarget(this.spec, target), + ); + + return inheritedMetadata; + } + + protected mergeWithOwn( + ownMetadata: MetadataMap, + target: Object, + methodName?: string, + methodDescriptor?: TypedPropertyDescriptor | number, + ) { + ownMetadata = ownMetadata || {}; + ownMetadata[methodName!] = this._mergeArray( + ownMetadata[methodName!], + this.withTarget(this.spec, target), + ); + return ownMetadata; + } + + private _mergeArray(result: T[], methodMeta: T | T[]) { + if (!result) { + if (Array.isArray(methodMeta)) { + result = methodMeta; + } else { + result = [methodMeta]; + } + } else { + if (Array.isArray(methodMeta)) { + result.push(...methodMeta); + } else { + result.push(methodMeta); + } + } + + return result; + } +}