Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions packages/metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>(
'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<object>(
'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<GeometryMetadata>(
'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<GeometryMetadata>(
'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
Expand Down
160 changes: 160 additions & 0 deletions packages/metadata/src/__tests__/unit/decorator-factory.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ClassDecoratorFactory,
DecoratorFactory,
MethodDecoratorFactory,
MethodMultiDecoratorFactory,
MethodParameterDecoratorFactory,
ParameterDecoratorFactory,
PropertyDecoratorFactory,
Expand Down Expand Up @@ -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)`
Expand Down
75 changes: 75 additions & 0 deletions packages/metadata/src/decorator-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,3 +789,78 @@ export class MethodParameterDecoratorFactory<T> 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<T> extends MethodDecoratorFactory<
T[]
> {
protected mergeWithInherited(
inheritedMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
) {
inheritedMetadata = inheritedMetadata || {};

inheritedMetadata[methodName!] = this._mergeArray(
inheritedMetadata[methodName!],
this.withTarget(this.spec, target),
);

return inheritedMetadata;
}

protected mergeWithOwn(
ownMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
ownMetadata = ownMetadata || {};
ownMetadata[methodName!] = this._mergeArray(
ownMetadata[methodName!],
this.withTarget(this.spec, target),
);
return ownMetadata;
}

private _mergeArray(result: T[], methodMeta: T | T[]) {
if (!result) {
if (Array.isArray(methodMeta)) {
result = methodMeta;
} else {
result = [methodMeta];
}
} else {
if (Array.isArray(methodMeta)) {
result.push(...methodMeta);
} else {
result.push(methodMeta);
}
}

return result;
}
}