Skip to content

Commit

Permalink
feat(core): add strongly-typed decorators factory (reflector)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Aug 16, 2023
1 parent 4fa5324 commit d274f54
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 7 deletions.
163 changes: 157 additions & 6 deletions packages/core/services/reflector.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import { Type } from '@nestjs/common';
import { CustomDecorator, SetMetadata, Type } from '@nestjs/common';
import { isEmpty, isObject } from '@nestjs/common/utils/shared.utils';
import { uid } from 'uid';

/**
* @publicApi
*/
export interface CreateDecoratorOptions<T = any> {
/**
* The key for the metadata.
* @default uid(21)
*/
key?: string;

/**
* The transform function to apply to the value.
* @default value => value
*/
transform?: (value: T) => T;
}

/**
* @publicApi
*/
export type ReflectableDecorator<T> = ((opts?: T) => CustomDecorator) & {
KEY: string;
};

/**
* Helper class providing Nest reflection capabilities.
Expand All @@ -9,6 +34,44 @@ import { isEmpty, isObject } from '@nestjs/common/utils/shared.utils';
* @publicApi
*/
export class Reflector {
/**
* Creates a decorator that can be used to decorate classes and methods with metadata.
* The decorator will also add the class to the collection of discoverable classes (by metadata key).
* Decorated classes can be discovered using the `getProviders` and `getControllers` methods.
* @param metadataKey The metadata key to use.
* @returns A decorator function.
*/
static createDecorator<T>(
options: CreateDecoratorOptions = {},
): ReflectableDecorator<T> {
const metadataKey = options.key ?? uid(21);
const decoratorFn =
(metadataValue: T) =>
(target: object | Function, key?: string | symbol, descriptor?: any) => {
const value = options.transform
? options.transform(metadataValue)
: metadataValue;
SetMetadata(metadataKey, value ?? {})(target, key, descriptor);
};

decoratorFn.KEY = metadataKey;
return decoratorFn as ReflectableDecorator<T>;
}

/**
* Retrieve metadata for a reflectable decorator for a specified target.
*
* @example
* `const roles = this.reflector.get(Roles, context.getHandler());`
*
* @param decorator reflectable decorator created through `Reflector.createDecorator`
* @param target context (decorated object) to retrieve metadata from
*
*/
public get<T extends ReflectableDecorator<any>>(
decorator: T,
target: Type<any> | Function,
): T extends ReflectableDecorator<infer R> ? R : unknown;
/**
* Retrieve metadata for a specified key for a specified target.
*
Expand All @@ -22,10 +85,43 @@ export class Reflector {
public get<TResult = any, TKey = any>(
metadataKey: TKey,
target: Type<any> | Function,
): TResult;
/**
* Retrieve metadata for a specified key or decorator for a specified target.
*
* @example
* `const roles = this.reflector.get<string[]>('roles', context.getHandler());`
*
* @param metadataKey lookup key or decorator for metadata to retrieve
* @param target context (decorated object) to retrieve metadata from
*
*/
public get<TResult = any, TKey = any>(
metadataKeyOrDecorator: TKey,
target: Type<any> | Function,
): TResult {
const metadataKey =
(metadataKeyOrDecorator as ReflectableDecorator<unknown>).KEY ??
metadataKeyOrDecorator;

return Reflect.getMetadata(metadataKey, target);
}

/**
* Retrieve metadata for a specified decorator for a specified set of targets.
*
* @param decorator lookup decorator for metadata to retrieve
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAll<T extends ReflectableDecorator<any>>(
decorator: T,
targets: (Type<any> | Function)[],
): T extends ReflectableDecorator<infer R>
? R extends Array<any>
? R[]
: R
: unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets.
*
Expand All @@ -36,25 +132,58 @@ export class Reflector {
public getAll<TResult extends any[] = any[], TKey = any>(
metadataKey: TKey,
targets: (Type<any> | Function)[],
): TResult;
/**
* Retrieve metadata for a specified key or decorator for a specified set of targets.
*
* @param metadataKeyOrDecorator lookup key or decorator for metadata to retrieve
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAll<TResult extends any[] = any[], TKey = any>(
metadataKeyOrDecorator: TKey,
targets: (Type<any> | Function)[],
): TResult {
return (targets || []).map(target =>
this.get(metadataKey, target),
this.get(metadataKeyOrDecorator, target),
) as TResult;
}

/**
* Retrieve metadata for a specified decorator for a specified set of targets and merge results.
*
* @param decorator lookup decorator for metadata to retrieve
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAllAndMerge<T extends ReflectableDecorator<any>>(
decorator: T,
targets: (Type<any> | Function)[],
): T extends ReflectableDecorator<infer R> ? R : unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets and merge results.
*
* @param metadataKey lookup key for metadata to retrieve
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAllAndMerge<TResult extends any[] = any[], TKey = any>(
public getAllAndMerge<TResult extends any[] | object = any[], TKey = any>(
metadataKey: TKey,
targets: (Type<any> | Function)[],
): TResult;
/**
* Retrieve metadata for a specified key or decorator for a specified set of targets and merge results.
*
* @param metadataKeyOrDecorator lookup key for metadata to retrieve
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAllAndMerge<TResult extends any[] | object = any[], TKey = any>(
metadataKeyOrDecorator: TKey,
targets: (Type<any> | Function)[],
): TResult {
const metadataCollection = this.getAll<TResult, TKey>(
metadataKey,
const metadataCollection = this.getAll<any[], TKey>(
metadataKeyOrDecorator,
targets,
).filter(item => item !== undefined);

Expand All @@ -75,6 +204,17 @@ export class Reflector {
});
}

/**
* Retrieve metadata for a specified decorator for a specified set of targets and return a first not undefined value.
*
* @param decorator lookup decorator for metadata to retrieve
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAllAndOverride<T extends ReflectableDecorator<any>>(
decorator: T,
targets: (Type<any> | Function)[],
): T extends ReflectableDecorator<infer R> ? R : unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets and return a first not undefined value.
*
Expand All @@ -85,9 +225,20 @@ export class Reflector {
public getAllAndOverride<TResult = any, TKey = any>(
metadataKey: TKey,
targets: (Type<any> | Function)[],
): TResult;
/**
* Retrieve metadata for a specified key or decorator for a specified set of targets and return a first not undefined value.
*
* @param metadataKeyOrDecorator lookup key or metadata for metadata to retrieve
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAllAndOverride<TResult = any, TKey = any>(
metadataKeyOrDecorator: TKey,
targets: (Type<any> | Function)[],
): TResult {
for (const target of targets) {
const result = this.get(metadataKey, target);
const result = this.get(metadataKeyOrDecorator, target);
if (result !== undefined) {
return result;
}
Expand Down
25 changes: 24 additions & 1 deletion packages/core/test/services/reflector.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,35 @@ describe('Reflector', () => {
reflector = new Reflector();
});
describe('get', () => {
it('should reflect metadata', () => {
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);
});
it('should reflect metadata by decorator', () => {
const decorator = Reflector.createDecorator<string>();
const value = 'value';
Reflect.defineMetadata(decorator.KEY, value, Test);

let reflectedValue = reflector.get(decorator, Test);
expect(reflectedValue).to.eql(value);

// @ts-expect-error 'value' is not assignable to parameter of type 'string'
reflectedValue = true;
});

it('should reflect metadata by decorator (custom key)', () => {
const decorator = Reflector.createDecorator<string[]>({ key: 'custom' });
const value = ['value'];
Reflect.defineMetadata('custom', value, Test);

let reflectedValue = reflector.get(decorator, Test);
expect(reflectedValue).to.eql(value);

// @ts-expect-error 'value' is not assignable to parameter of type 'string[]'
reflectedValue = true;
});
});

describe('getAll', () => {
Expand Down

0 comments on commit d274f54

Please sign in to comment.