refactor(): use ExternalContextCreator

This commit is contained in:
Morb0 2021-01-03 02:39:50 +03:00
parent d7f397b375
commit 798494a5b2
12 changed files with 86 additions and 508 deletions

View File

@ -1,38 +0,0 @@
import { NestContainer } from '@nestjs/core';
import { BaseExceptionFilterContext } from '@nestjs/core/exceptions/base-exception-filter-context';
import { EXCEPTION_FILTERS_METADATA } from '@nestjs/common/constants';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { TelegrafExceptionsHandler } from '../exceptions/telegraf-exceptions-handler';
export class FiltersContextCreator extends BaseExceptionFilterContext {
constructor(container: NestContainer) {
super(container);
}
public create(
instance: object,
callback: (...args: any[]) => void,
moduleKey: string,
): TelegrafExceptionsHandler {
this.moduleContext = moduleKey;
const exceptionHandler = new TelegrafExceptionsHandler();
const filters = this.createContext(
instance,
callback,
EXCEPTION_FILTERS_METADATA,
);
if (isEmpty(filters)) {
return exceptionHandler;
}
exceptionHandler.setCustomFilters(filters.reverse());
return exceptionHandler;
}
public getGlobalMetadata<T extends any[]>(): T {
return [] as T;
}
}

View File

@ -1,278 +0,0 @@
import { Controller, PipeTransform } from '@nestjs/common/interfaces';
import { PipesContextCreator } from '@nestjs/core/pipes/pipes-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
import { GuardsContextCreator } from '@nestjs/core/guards/guards-context-creator';
import { GuardsConsumer } from '@nestjs/core/guards/guards-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import {
ContextUtils,
ParamProperties,
} from '@nestjs/core/helpers/context-utils';
import { HandlerMetadataStorage } from '@nestjs/core/helpers/handler-metadata-storage';
import { FORBIDDEN_MESSAGE } from '@nestjs/core/guards/constants';
import { ParamsMetadata } from '@nestjs/core/helpers/interfaces';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { FiltersContextCreator } from './filters-context-creator';
import { TelegrafContextType } from '../execution-context/telegraf-execution-context';
import { TelegrafProxy } from './telegraf-proxy';
import { TelegrafException } from '../errors';
import {
CUSTOM_LISTENER_AGRS_METADATA,
LISTENER_ARGS_METADATA,
} from '../telegraf.constants';
import { TelegrafParamsFactory } from '../factories/telegraf-params-factory';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
export type Update = Controller;
type TelegrafParamProperties = ParamProperties & { metatype?: any };
export interface TelegrafHandlerMetadata {
argsLength: number;
paramtypes: any[];
getParamsMetadata: (moduleKey: string) => TelegrafParamProperties[];
}
export class TelegrafContextCreator {
private readonly contextUtils = new ContextUtils();
private readonly telegrafParamsFactory = new TelegrafParamsFactory();
private readonly handlerMetadataStorage = new HandlerMetadataStorage<TelegrafHandlerMetadata>();
constructor(
private readonly telegrafProxy: TelegrafProxy,
private readonly exceptionFiltersContext: FiltersContextCreator,
private readonly pipesContextCreator: PipesContextCreator,
private readonly pipesConsumer: PipesConsumer,
private readonly guardsContextCreator: GuardsContextCreator,
private readonly guardsConsumer: GuardsConsumer,
private readonly interceptorsContextCreator: InterceptorsContextCreator,
private readonly interceptorsConsumer: InterceptorsConsumer,
) {}
public create<T extends ParamsMetadata = ParamsMetadata>(
instance: Update,
methodRef: (...args: unknown[]) => void,
moduleName: string,
methodKey: string,
): (...args: any[]) => Promise<void> {
const contextType: TelegrafContextType = 'telegraf';
const { argsLength, paramtypes, getParamsMetadata } = this.getMetadata<T>(
instance,
methodKey,
contextType,
);
const exceptionHandler = this.exceptionFiltersContext.create(
instance,
methodRef,
moduleName,
);
const pipes = this.pipesContextCreator.create(
instance,
methodRef,
moduleName,
);
const guards = this.guardsContextCreator.create(
instance,
methodRef,
moduleName,
);
const interceptors = this.interceptorsContextCreator.create(
instance,
methodRef,
moduleName,
);
const paramsMetadata = getParamsMetadata(moduleName);
const paramsOptions = paramsMetadata
? this.contextUtils.mergeParamsMetatypes(paramsMetadata, paramtypes)
: [];
const fnApplyPipes = this.createPipesFn(pipes, paramsOptions);
const fnCanActivate = this.createGuardsFn(
guards,
instance,
methodRef,
contextType,
);
const handler = <TContext>(
initialArgs: unknown[],
ctx: TContext,
next: Function,
) => async () => {
if (fnApplyPipes) {
await fnApplyPipes(initialArgs, ctx, next);
return methodRef.apply(instance, initialArgs);
}
return methodRef.apply(instance, [ctx, next]);
};
const targetCallback = async <TContext>(ctx: TContext, next: Function) => {
const initialArgs = this.contextUtils.createNullArray(argsLength);
fnCanActivate && (await fnCanActivate([ctx, next]));
return this.interceptorsConsumer.intercept(
interceptors,
[ctx, next],
instance,
methodRef,
handler(initialArgs, ctx, next),
contextType,
);
};
return this.telegrafProxy.create(targetCallback, exceptionHandler);
}
public getMetadata<
TMetadata,
TContext extends TelegrafContextType = TelegrafContextType
>(
instance: Controller,
methodName: string,
contextType: TContext,
): TelegrafHandlerMetadata {
const cachedMetadata = this.handlerMetadataStorage.get(
instance,
methodName,
);
if (cachedMetadata) return cachedMetadata;
const metadata =
this.contextUtils.reflectCallbackMetadata<TMetadata>(
instance,
methodName,
LISTENER_ARGS_METADATA,
) || {};
const keys = Object.keys(metadata);
const argsLength = this.contextUtils.getArgumentsLength(keys, metadata);
const contextFactory = this.contextUtils.getContextFactory(
contextType,
instance,
instance[methodName],
);
const getParamsMetadata = (moduleKey: string) =>
this.exchangeKeysForValues(
keys,
metadata,
moduleKey,
this.telegrafParamsFactory,
contextFactory,
);
const paramtypes = this.contextUtils.reflectCallbackParamtypes(
instance,
methodName,
);
const handlerMetadata: TelegrafHandlerMetadata = {
argsLength,
paramtypes,
getParamsMetadata,
};
this.handlerMetadataStorage.set(instance, methodName, handlerMetadata);
return handlerMetadata;
}
public exchangeKeysForValues<TMetadata = any>(
keys: string[],
metadata: TMetadata,
moduleContext: string,
paramsFactory: TelegrafParamsFactory,
contextFactory: (args: unknown[]) => ExecutionContextHost,
): ParamProperties[] {
this.pipesContextCreator.setModuleContext(moduleContext);
return keys.map((key) => {
const { index, data, pipes: pipesCollection } = metadata[key];
const pipes = this.pipesContextCreator.createConcreteContext(
pipesCollection,
);
const type = this.contextUtils.mapParamType(key);
if (key.includes(CUSTOM_LISTENER_AGRS_METADATA)) {
const { factory } = metadata[key];
const customExtractValue = this.contextUtils.getCustomFactory(
factory,
data,
contextFactory,
);
return { index, extractValue: customExtractValue, type, data, pipes };
}
const numericType = Number(type);
const extractValue = <TContext>(ctx: TContext, next: Function) =>
paramsFactory.exchangeKeyForValue(numericType, ctx, next);
return { index, extractValue, type: numericType, data, pipes };
});
}
public createGuardsFn<TContext extends string = TelegrafContextType>(
guards: any[],
instance: Controller,
callback: (...args: unknown[]) => any,
contextType?: TContext,
): Function | null {
const canActivateFn = async (args: any[]) => {
const canActivate = await this.guardsConsumer.tryActivate<TContext>(
guards,
args,
instance,
callback,
contextType,
);
if (!canActivate) {
throw new TelegrafException(FORBIDDEN_MESSAGE);
}
};
return guards.length ? canActivateFn : null;
}
public createPipesFn(
pipes: PipeTransform[],
paramsOptions: (ParamProperties & { metatype?: unknown })[],
) {
const pipesFn = async <TContext>(
args: unknown[],
ctx: TContext,
next: Function,
) => {
const resolveParamValue = async (
param: ParamProperties & { metatype?: unknown },
) => {
const {
index,
extractValue,
type,
data,
metatype,
pipes: paramPipes,
} = param;
const value = extractValue(ctx, next);
args[index] = await this.getParamValue(
value,
{ metatype, type, data },
pipes.concat(paramPipes),
);
};
await Promise.all(paramsOptions.map(resolveParamValue));
};
return paramsOptions.length ? pipesFn : null;
}
public async getParamValue<T>(
value: T,
{ metatype, type, data }: { metatype: any; type: any; data: any },
pipes: PipeTransform[],
): Promise<any> {
return isEmpty(pipes)
? value
: this.pipesConsumer.apply(value, { metatype, type, data }, pipes);
}
}

View File

@ -1,38 +0,0 @@
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { catchError } from 'rxjs/operators';
import { EMPTY } from 'rxjs';
import { isObservable } from '../helpers/is-observable.helper';
import { TelegrafExceptionsHandler } from '../exceptions/telegraf-exceptions-handler';
export class TelegrafProxy {
public create(
targetCallback: <TContext>(ctx: TContext, next: Function) => Promise<any>,
exceptionsHandler: TelegrafExceptionsHandler,
): <TContext>(ctx: TContext, next: Function) => Promise<any> {
return async <TContext>(ctx: TContext, next: Function) => {
try {
const result = await targetCallback(ctx, next);
return !isObservable(result)
? result
: result.pipe(
catchError((error) => {
this.handleError(exceptionsHandler, [ctx, next], error);
return EMPTY;
}),
);
} catch (error) {
this.handleError(exceptionsHandler, [ctx, next], error);
}
};
}
handleError<T>(
exceptionsHandler: TelegrafExceptionsHandler,
args: unknown[],
error: T,
): void {
const host = new ExecutionContextHost(args);
host.setType('telegraf');
exceptionsHandler.handle(error, host);
}
}

View File

@ -1,15 +1,10 @@
import { PipeTransform, Type } from '@nestjs/common';
import { createPipesTelegrafParamDecorator } from '../../utils/param-decorator.util';
import { createTelegrafPipesParamDecorator } from '../../utils/param-decorator.util';
import { TelegrafParamtype } from '../../enums/telegraf-paramtype.enum';
export function MessageText(): ParameterDecorator;
export function MessageText(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
export function MessageText(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
return createPipesTelegrafParamDecorator(TelegrafParamtype.MESSAGE_TEXT)(
export function MessageText(...pipes: (Type<PipeTransform> | PipeTransform)[]) {
return createTelegrafPipesParamDecorator(TelegrafParamtype.MESSAGE_TEXT)(
undefined,
...pipes,
);
}

View File

@ -1,34 +0,0 @@
import { ArgumentsHost, Logger } from '@nestjs/common';
import { MESSAGES } from '@nestjs/core/constants';
import { Context } from 'telegraf';
import { TelegrafExceptionFilter } from '../interfaces/telegraf-exception-filter.interface';
import { TelegrafException } from '../errors';
import { isErrorObject } from '../helpers/is-error-object.helper';
import { TelegrafArgumentsHost } from '../execution-context';
export class BaseTelegrafExceptionFilter<TError = any>
implements TelegrafExceptionFilter {
private static readonly logger = new Logger('TelegrafExceptionsHandler');
catch(exception: TError, host: ArgumentsHost): void {
const context = TelegrafArgumentsHost.create(host).getContext<Context>();
this.handleError(exception, context);
}
public handleError(exception: TError, context: Context): void {
if (!(exception instanceof TelegrafException)) {
return this.handleUnknownError(exception, context);
}
context.reply(exception.message);
}
public handleUnknownError(exception: TError, context: Context): void {
context.reply(MESSAGES.UNKNOWN_EXCEPTION_MESSAGE);
const errorMessage = isErrorObject(exception)
? exception.message
: exception;
BaseTelegrafExceptionFilter.logger.error(errorMessage);
}
}

View File

@ -1 +0,0 @@
export * from './base-telegraf-exception-filter';

View File

@ -1,48 +0,0 @@
import { ArgumentsHost } from '@nestjs/common';
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions';
import { BaseTelegrafExceptionFilter } from './base-telegraf-exception-filter';
import { TelegrafException } from '../errors';
import { InvalidExceptionFilterException } from '@nestjs/core/errors/exceptions/invalid-exception-filter.exception';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
export class TelegrafExceptionsHandler extends BaseTelegrafExceptionFilter {
private filters: ExceptionFilterMetadata[] = [];
public handle(
exception: Error | TelegrafException | any,
host: ArgumentsHost,
): void {
const isFilterInvoked = this.invokeCustomFilters(exception, host);
if (!isFilterInvoked) {
super.catch(exception, host);
}
}
public invokeCustomFilters<T = any>(
exception: T,
args: ArgumentsHost,
): boolean {
if (isEmpty(this.filters)) return false;
const filter = this.filters.find(({ exceptionMetatypes }) => {
const hasMetatype =
!exceptionMetatypes.length ||
exceptionMetatypes.some(
(ExceptionMetatype) => exception instanceof ExceptionMetatype,
);
return hasMetatype;
});
filter && filter.func(exception, args);
return !!filter;
}
public setCustomFilters(filters: ExceptionFilterMetadata[]): void {
if (!Array.isArray(filters)) {
throw new InvalidExceptionFilterException();
}
this.filters = filters;
}
}

View File

@ -3,25 +3,18 @@ import { Injectable as IInjectable } from '@nestjs/common/interfaces/injectable.
import { DiscoveryService, ModuleRef, ModulesContainer } from '@nestjs/core';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { PipesContextCreator } from '@nestjs/core/pipes/pipes-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
import { GuardsContextCreator } from '@nestjs/core/guards/guards-context-creator';
import { GuardsConsumer } from '@nestjs/core/guards/guards-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils';
import { fromPromise } from 'rxjs/internal-compatibility';
import { filter, mergeAll } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { Context, Telegraf } from 'telegraf';
import { TelegrafMetadataAccessor } from '../telegraf.metadata-accessor';
import { TelegrafContextCreator } from '../context/telegraf-context-creator';
import { TelegrafProxy } from '../context/telegraf-proxy';
import { FiltersContextCreator } from '../context/filters-context-creator';
import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator';
import { TelegrafParamsFactory } from '../factories/telegraf-params-factory';
@Injectable()
export class TelegrafUpdateExplorer implements OnModuleInit {
private readonly contextCreator: TelegrafContextCreator;
private readonly telegrafParamsFactory = new TelegrafParamsFactory();
constructor(
private readonly moduleRef: ModuleRef,
@ -29,24 +22,9 @@ export class TelegrafUpdateExplorer implements OnModuleInit {
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: TelegrafMetadataAccessor,
private readonly metadataScanner: MetadataScanner,
private readonly externalContextCreator: ExternalContextCreator,
@Inject(Telegraf) private readonly telegraf: Telegraf<never>,
) {
this.contextCreator = this.getContextCreator();
}
private getContextCreator(): TelegrafContextCreator {
const { container } = this.moduleRef as any;
return new TelegrafContextCreator(
new TelegrafProxy(),
new FiltersContextCreator(container),
new PipesContextCreator(container),
new PipesConsumer(),
new GuardsContextCreator(container),
new GuardsConsumer(),
new InterceptorsContextCreator(container),
new InterceptorsConsumer(),
);
}
) {}
onModuleInit(): void {
this.explore();
@ -87,11 +65,20 @@ export class TelegrafUpdateExplorer implements OnModuleInit {
moduleName: string,
): void {
const methodRef = instance[methodKey] as (...args: unknown[]) => unknown;
const contextHandlerFn = this.contextCreator.create(
const contextHandlerFn = this.externalContextCreator.create(
instance,
methodRef,
moduleName,
methodKey,
this.telegrafParamsFactory,
null,
null,
{
interceptors: true,
filters: true,
guards: true,
},
'telegraf',
);
const listenerMetadata = this.metadataAccessor.getListenerMetadata(

View File

@ -1,5 +0,0 @@
import { isObject } from '@nestjs/common/utils/shared.utils';
export function isErrorObject(err: any): err is Error {
return isObject(err) && !!(err as Error).message;
}

View File

@ -1,5 +0,0 @@
import { isFunction } from '@nestjs/common/utils/shared.utils';
export function isObservable(result: any): boolean {
return result && isFunction(result.subscribe);
}

View File

@ -1,7 +1,4 @@
import {
CUSTOM_ROUTE_AGRS_METADATA,
ROUTE_ARGS_METADATA,
} from '@nestjs/common/constants';
import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS';
@ -10,5 +7,4 @@ export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA';
export const SCENE_METADATA = 'SCENE_METADATA';
export const LISTENER_ARGS_METADATA = ROUTE_ARGS_METADATA;
export const CUSTOM_LISTENER_AGRS_METADATA = CUSTOM_ROUTE_AGRS_METADATA;
export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA;

View File

@ -1,39 +1,86 @@
import { assignMetadata, PipeTransform, Type } from '@nestjs/common';
import { isNil, isString } from '@nestjs/common/utils/shared.utils';
import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum';
import { LISTENER_ARGS_METADATA } from '../telegraf.constants';
import { PARAM_ARGS_METADATA } from '../telegraf.constants';
export function createTelegrafParamDecorator(
paramtype: TelegrafParamtype,
): (...pipes: (Type<PipeTransform> | PipeTransform)[]) => ParameterDecorator {
return (...pipes: (Type<PipeTransform> | PipeTransform)[]) => (
target,
key,
index,
) => {
export type ParamData = object | string | number;
export const createTelegrafParamDecorator = (paramtype: TelegrafParamtype) => {
return (data?: ParamData): ParameterDecorator => (target, key, index) => {
const args =
Reflect.getMetadata(LISTENER_ARGS_METADATA, target.constructor, key) ||
{};
Reflect.getMetadata(PARAM_ARGS_METADATA, target.constructor, key) || {};
Reflect.defineMetadata(
LISTENER_ARGS_METADATA,
assignMetadata(args, paramtype, index, undefined, ...pipes),
PARAM_ARGS_METADATA,
assignMetadata(args, paramtype, index, data),
target.constructor,
key,
);
};
}
};
export const createPipesTelegrafParamDecorator = (
export const createTelegrafPipesParamDecorator = (
paramtype: TelegrafParamtype,
) => (
data?: any,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator => (target, key, index) => {
addPipesMetadata(paramtype, data, pipes, target, key, index);
};
export const addPipesMetadata = (
paramtype: TelegrafParamtype,
data: any,
pipes: (Type<PipeTransform> | PipeTransform)[],
target: Record<string, any>,
key: string | symbol,
index: number,
) => {
const args =
Reflect.getMetadata(LISTENER_ARGS_METADATA, target.constructor, key) || {};
Reflect.getMetadata(PARAM_ARGS_METADATA, target.constructor, key) || {};
const hasParamData = isNil(data) || isString(data);
const paramData = hasParamData ? data : undefined;
const paramPipes = hasParamData ? pipes : [data, ...pipes];
Reflect.defineMetadata(
LISTENER_ARGS_METADATA,
assignMetadata(args, paramtype, index, undefined, ...pipes),
PARAM_ARGS_METADATA,
assignMetadata(args, paramtype, index, paramData, ...paramPipes),
target.constructor,
key,
);
};
// export function createTelegrafParamDecorator(
// paramtype: TelegrafParamtype,
// ): (...pipes: (Type<PipeTransform> | PipeTransform)[]) => ParameterDecorator {
// return (...pipes: (Type<PipeTransform> | PipeTransform)[]) => (
// target,
// key,
// index,
// ) => {
// const args =
// Reflect.getMetadata(LISTENER_ARGS_METADATA, target.constructor, key) ||
// {};
// Reflect.defineMetadata(
// LISTENER_ARGS_METADATA,
// assignMetadata(args, paramtype, index, undefined, ...pipes),
// target.constructor,
// key,
// );
// };
// }
//
// export const createPipesTelegrafParamDecorator = (
// paramtype: TelegrafParamtype,
// ) => (
// ...pipes: (Type<PipeTransform> | PipeTransform)[]
// ): ParameterDecorator => (target, key, index) => {
// const args =
// Reflect.getMetadata(LISTENER_ARGS_METADATA, target.constructor, key) || {};
//
// Reflect.defineMetadata(
// LISTENER_ARGS_METADATA,
// assignMetadata(args, paramtype, index, undefined, ...pipes),
// target.constructor,
// key,
// );
// };