Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add strongly-typed decorators factory (reflector) #12237

Merged
merged 5 commits into from
Aug 18, 2023
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
162 changes: 156 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 metadata 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,43 @@ 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.
* Can be used as a strongly-typed alternative to `@SetMetadata`.
* @param options Decorator options.
* @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 +84,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 +131,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 +203,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 +224,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
2 changes: 1 addition & 1 deletion sample/01-cats-app/src/cats/cats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class CatsController {
constructor(private readonly catsService: CatsService) {}

@Post()
@Roles('admin')
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
Expand Down
4 changes: 2 additions & 2 deletions sample/01-cats-app/src/common/decorators/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
export const Roles = Reflector.createDecorator<string[]>();
3 changes: 2 additions & 1 deletion sample/01-cats-app/src/common/guards/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
Expand Down
Loading