fix(core): fix type and return value of Reflector#getAllAndMerge

When only a single metadata value is present in the list of targets for
`Reflector#getAllAndMerge`, that value gets wrapped in an array if it is
not already an array or an object.
Previously, single values were returned as is, so the method could
actually return any value. Now the method always returns an array or an
object as indicated by its return type.
This also fixes the return type of both `getAll` and `getAllAndMerge`
when using the strongly typed `ReflectableDecorator<T>`.

Fixes #12231
This commit is contained in:
René Zeidler
2023-08-16 10:59:46 +02:00
parent 6bacb74849
commit d04886d816
2 changed files with 104 additions and 37 deletions

View File

@@ -118,8 +118,8 @@ export class Reflector {
targets: (Type<any> | Function)[],
): T extends ReflectableDecorator<infer R>
? R extends Array<any>
? R[]
: R
? R
: R[]
: unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets.
@@ -158,7 +158,13 @@ export class Reflector {
public getAllAndMerge<T extends ReflectableDecorator<any>>(
decorator: T,
targets: (Type<any> | Function)[],
): T extends ReflectableDecorator<infer R> ? R : unknown;
): T extends ReflectableDecorator<infer R>
? R extends Array<any>
? R
: R extends object
? R | []
: R[]
: unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets and merge results.
*
@@ -189,6 +195,13 @@ export class Reflector {
if (isEmpty(metadataCollection)) {
return metadataCollection as TResult;
}
if (metadataCollection.length === 1) {
const value = metadataCollection[0];
if (isObject(value)) {
return value as TResult;
}
return metadataCollection as TResult;
}
return metadataCollection.reduce((a, b) => {
if (Array.isArray(a)) {
return a.concat(b);

View File

@@ -1,25 +1,36 @@
import { expect } from 'chai';
import { Reflector } from '../../services/reflector.service';
type TestObject = {
only1?: string;
only2?: string;
both: string;
};
describe('Reflector', () => {
const key = 'key';
let reflector: Reflector;
class Test {}
class Test1 {}
class Test2 {}
beforeEach(() => {
Reflect.deleteMetadata(key, Test1);
Reflect.deleteMetadata(key, Test2);
reflector = new Reflector();
});
describe('get', () => {
it('should reflect metadata by key', () => {
const key = 'key';
const value = 'value';
Reflect.defineMetadata(key, value, Test);
expect(reflector.get(key, Test)).to.eql(value);
Reflect.defineMetadata(key, value, Test1);
expect(reflector.get(key, Test1)).to.eql(value);
});
it('should reflect metadata by decorator', () => {
const decorator = Reflector.createDecorator<string>();
const value = 'value';
Reflect.defineMetadata(decorator.KEY, value, Test);
Reflect.defineMetadata(decorator.KEY, value, Test1);
let reflectedValue = reflector.get(decorator, Test);
// string
let reflectedValue = reflector.get(decorator, Test1);
expect(reflectedValue).to.eql(value);
// @ts-expect-error 'value' is not assignable to parameter of type 'string'
@@ -29,9 +40,10 @@ describe('Reflector', () => {
it('should reflect metadata by decorator (custom key)', () => {
const decorator = Reflector.createDecorator<string[]>({ key: 'custom' });
const value = ['value'];
Reflect.defineMetadata('custom', value, Test);
Reflect.defineMetadata('custom', value, Test1);
let reflectedValue = reflector.get(decorator, Test);
// string[]
let reflectedValue = reflector.get(decorator, Test1);
expect(reflectedValue).to.eql(value);
// @ts-expect-error 'value' is not assignable to parameter of type 'string[]'
@@ -40,53 +52,95 @@ describe('Reflector', () => {
});
describe('getAll', () => {
it('should reflect metadata of all targets', () => {
const key = 'key';
const value = 'value';
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAll(key, [Test])).to.eql([value]);
it('should reflect metadata of all targets by key', () => {
const value1 = 'value1';
const value2 = 'value2';
Reflect.defineMetadata(key, value1, Test1);
Reflect.defineMetadata(key, value2, Test2);
expect(reflector.getAll(key, [Test1, Test2])).to.eql([value1, value2]);
});
it('should reflect metadata of all targets by decorator', () => {
const decorator = Reflector.createDecorator<string>();
const value1 = 'value1';
const value2 = 'value2';
Reflect.defineMetadata(decorator.KEY, value1, Test1);
Reflect.defineMetadata(decorator.KEY, value2, Test2);
// string[]
const reflectedValue = reflector.getAll(decorator, [Test1, Test2]);
expect(reflectedValue).to.eql([value1, value2]);
});
});
describe('getAllAndMerge', () => {
it('should return an empty array when there are no targets', () => {
const key = 'key';
expect(reflector.getAllAndMerge(key, [])).to.be.empty;
});
it('should reflect metadata of all targets and concat arrays', () => {
const key = 'key';
const decorator = Reflector.createDecorator<string[]>();
const value = 'value';
Reflect.defineMetadata(key, [value], Test);
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql([
value,
value,
Reflect.defineMetadata(decorator.KEY, [value], Test1);
// string[]
const reflectedValue = reflector.getAllAndMerge(decorator, [
Test1,
Test1,
]);
expect(reflectedValue).to.eql([value, value]);
});
it('should reflect metadata of all targets and create an array', () => {
const key = 'key';
const decorator = Reflector.createDecorator<string>();
const value = 'value';
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql([
value,
value,
Reflect.defineMetadata(decorator.KEY, value, Test1);
// string[]
const reflectedValue = reflector.getAllAndMerge(decorator, [
Test1,
Test1,
]);
expect(reflectedValue).to.eql([value, value]);
});
it('should reflect metadata of all targets and merge an object', () => {
const key = 'key';
const value = { test: 'test' };
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql({
...value,
it('should reflect metadata of all targets and merge objects', () => {
const decorator = Reflector.createDecorator<TestObject>();
const value1: TestObject = { only1: 'test1', both: 'overriden' };
const value2: TestObject = { only2: 'test2', both: 'test' };
Reflect.defineMetadata(decorator.KEY, value1, Test1);
Reflect.defineMetadata(decorator.KEY, value2, Test2);
// [] | TestObject
const reflectedValue = reflector.getAllAndMerge(decorator, [
Test1,
Test2,
]);
expect(reflectedValue).to.eql({
...value1,
...value2,
});
});
it('should reflect metadata of all targets and create an array from a single value', () => {
const value = 'value';
Reflect.defineMetadata(key, value, Test1);
expect(reflector.getAllAndMerge(key, [Test1, Test2])).to.eql([value]);
});
it('should reflect metadata of all targets and return a single array unmodified', () => {
const value = ['value'];
Reflect.defineMetadata(key, value, Test1);
expect(reflector.getAllAndMerge(key, [Test1, Test2])).to.eql(value);
});
it('should reflect metadata of all targets and return a single object unmodified', () => {
const value = { test: 'value' };
Reflect.defineMetadata(key, value, Test1);
expect(reflector.getAllAndMerge(key, [Test1, Test2])).to.eql(value);
});
});
describe('getAllAndOverride', () => {
it('should reflect metadata of all targets and return a first not undefined value', () => {
const key = 'key';
const value = 'value';
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAllAndOverride(key, [Test, Test])).to.eql(value);
const value1 = 'value1';
const value2 = 'value2';
Reflect.defineMetadata(key, value1, Test1);
Reflect.defineMetadata(key, value2, Test2);
expect(reflector.getAllAndOverride(key, [Test1, Test2])).to.eql(value1);
});
});
});