mirror of
https://github.com/Maks1mS/nestjs-telegraf.git
synced 2025-12-20 03:59:54 +03:00
feat!(): add custom execution context
This commit is contained in:
38
lib/context/filters-context-creator.ts
Normal file
38
lib/context/filters-context-creator.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
278
lib/context/telegraf-context-creator.ts
Normal file
278
lib/context/telegraf-context-creator.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
38
lib/context/telegraf-proxy.ts
Normal file
38
lib/context/telegraf-proxy.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user