@nestjs/microservices
Version:
Nest - modern, fast, powerful node.js web framework (@microservices)
202 lines (201 loc) • 10.7 kB
JavaScript
import { CUSTOM_ROUTE_ARGS_METADATA, isEmptyArray, isObject, PARAMTYPES_METADATA, } from '@nestjs/common/internal';
import { ContextUtils, ExecutionContextHost, FORBIDDEN_MESSAGE, HandlerMetadataStorage, STATIC_CONTEXT, } from '@nestjs/core/internal';
import { defer, isObservable, ReplaySubject } from 'rxjs';
import { PARAM_ARGS_METADATA } from '../constants.js';
import { RpcException } from '../exceptions/index.js';
import { RpcParamsFactory } from '../factories/rpc-params-factory.js';
import { DEFAULT_CALLBACK_METADATA } from './rpc-metadata-constants.js';
/**
* Notifiers that emit once the handler itself has returned (or resolved to)
* an Observable, keyed by the context (the second argument) it was called
* with. Only kept when interceptors or pre-request hooks hand back an
* Observable in place of its result.
*/
const handlerObservableNotifiers = new WeakMap();
/**
* Returns a notifier that emits once the handler called with `context` has
* returned (or resolved to) an Observable. Returns `undefined` when the
* result of the handler is not wrapped by interceptors or pre-request hooks,
* which leaves the caller to look at it directly.
*/
export function getHandlerObservableNotifier(context) {
return isObject(context)
? handlerObservableNotifiers.get(context)
: undefined;
}
export class RpcContextCreator {
rpcProxy;
exceptionFiltersContext;
pipesContextCreator;
pipesConsumer;
guardsContextCreator;
guardsConsumer;
interceptorsContextCreator;
interceptorsConsumer;
applicationConfig;
contextUtils = new ContextUtils();
rpcParamsFactory = new RpcParamsFactory();
handlerMetadataStorage = new HandlerMetadataStorage();
constructor(rpcProxy, exceptionFiltersContext, pipesContextCreator, pipesConsumer, guardsContextCreator, guardsConsumer, interceptorsContextCreator, interceptorsConsumer, applicationConfig) {
this.rpcProxy = rpcProxy;
this.exceptionFiltersContext = exceptionFiltersContext;
this.pipesContextCreator = pipesContextCreator;
this.pipesConsumer = pipesConsumer;
this.guardsContextCreator = guardsContextCreator;
this.guardsConsumer = guardsConsumer;
this.interceptorsContextCreator = interceptorsContextCreator;
this.interceptorsConsumer = interceptorsConsumer;
this.applicationConfig = applicationConfig;
}
create(instance, callback, moduleKey, methodName, contextId = STATIC_CONTEXT, inquirerId, defaultCallMetadata = DEFAULT_CALLBACK_METADATA, reportUnhandledErrors = false) {
const contextType = 'rpc';
const { argsLength, paramtypes, getParamsMetadata } = this.getMetadata(instance, methodName, defaultCallMetadata, contextType);
const exceptionHandler = this.exceptionFiltersContext.create(instance, callback, moduleKey, contextId, inquirerId);
const pipes = this.pipesContextCreator.create(instance, callback, moduleKey, contextId, inquirerId);
const guards = this.guardsContextCreator.create(instance, callback, moduleKey, contextId, inquirerId);
const interceptors = this.interceptorsContextCreator.create(instance, callback, moduleKey, contextId, inquirerId);
const paramsMetadata = getParamsMetadata(moduleKey);
const paramsOptions = paramsMetadata
? this.contextUtils.mergeParamsMetatypes(paramsMetadata, paramtypes)
: [];
const fnApplyPipes = this.createPipesFn(pipes, paramsOptions);
const fnCanActivate = this.createGuardsFn(guards, instance, callback, contextType);
const handler = (initialArgs, args, handlerObservable$) => async () => {
fnApplyPipes && (await fnApplyPipes(initialArgs, ...args));
const result = callback.apply(instance, fnApplyPipes ? initialArgs : args);
handlerObservable$ &&
this.notifyIfObservable(result, handlerObservable$);
return result;
};
const preRequestHooks = this.applicationConfig?.getGlobalPreRequestHooks() ?? [];
// Interceptors and pre-request hooks always hand back an Observable, so
// whether the handler itself returned one can only be observed where it
// runs. Transports read it with "getHandlerObservableNotifier".
const isResultWrapped = !isEmptyArray(interceptors) || preRequestHooks.length > 0;
const targetCallback = async (...args) => {
const initialArgs = this.contextUtils.createNullArray(argsLength);
const handlerObservable$ = isResultWrapped
? this.attachHandlerObservableNotifier(args[1])
: undefined;
const executePipeline = async () => {
fnCanActivate && (await fnCanActivate(args));
return this.interceptorsConsumer.intercept(interceptors, args, instance, callback, handler(initialArgs, args, handlerObservable$), contextType);
};
if (preRequestHooks.length === 0) {
return executePipeline();
}
const executionContext = new ExecutionContextHost(args, instance.constructor, callback);
executionContext.setType(contextType);
// With no interceptors registered, `InterceptorsConsumer#intercept`
// resolves to whatever the handler returned rather than to an observable,
// so the result has to be wrapped rather than merged.
// `InterceptorsConsumer#transformDeferred` does exactly that, and also
// skips subscribing the producer once the consumer has unsubscribed.
const pipelineObs = defer(() => this.interceptorsConsumer.transformDeferred(executePipeline));
let index = 0;
const next = () => {
if (index >= preRequestHooks.length)
return pipelineObs;
return preRequestHooks[index++](executionContext, next);
};
return next();
};
return this.rpcProxy.create(targetCallback, exceptionHandler, reportUnhandledErrors);
}
reflectCallbackParamtypes(instance, callback) {
return Reflect.getMetadata(PARAMTYPES_METADATA, instance, callback.name);
}
createGuardsFn(guards, instance, callback, contextType) {
const canActivateFn = async (args) => {
const canActivate = await this.guardsConsumer.tryActivate(guards, args, instance, callback, contextType);
if (!canActivate) {
throw new RpcException(FORBIDDEN_MESSAGE);
}
};
return guards.length ? canActivateFn : null;
}
getMetadata(instance, methodName, defaultCallMetadata, contextType) {
const cacheMetadata = this.handlerMetadataStorage.get(instance, methodName);
if (cacheMetadata) {
return cacheMetadata;
}
const metadata = this.contextUtils.reflectCallbackMetadata(instance, methodName, PARAM_ARGS_METADATA) || defaultCallMetadata;
const keys = Object.keys(metadata);
const argsLength = this.contextUtils.getArgumentsLength(keys, metadata);
const paramtypes = this.contextUtils.reflectCallbackParamtypes(instance, methodName);
const contextFactory = this.contextUtils.getContextFactory(contextType, instance, instance[methodName]);
const getParamsMetadata = (moduleKey) => this.exchangeKeysForValues(keys, metadata, moduleKey, this.rpcParamsFactory, contextFactory);
const handlerMetadata = {
argsLength,
paramtypes,
getParamsMetadata,
};
this.handlerMetadataStorage.set(instance, methodName, handlerMetadata);
return handlerMetadata;
}
exchangeKeysForValues(keys, metadata, moduleContext, paramsFactory, contextFactory) {
this.pipesContextCreator.setModuleContext(moduleContext);
return keys.map(key => {
const { index, data, pipes: pipesCollection, schema } = metadata[key];
const pipes = this.pipesContextCreator.createConcreteContext(pipesCollection);
const type = this.contextUtils.mapParamType(key);
if (key.includes(CUSTOM_ROUTE_ARGS_METADATA)) {
const { factory } = metadata[key];
const customExtractValue = this.contextUtils.getCustomFactory(factory, data, contextFactory);
return {
index,
extractValue: customExtractValue,
type,
data,
pipes,
schema,
};
}
const numericType = Number(type);
const extractValue = (...args) => paramsFactory.exchangeKeyForValue(numericType, data, args);
return { index, extractValue, type: numericType, data, pipes, schema };
});
}
createPipesFn(pipes, paramsOptions) {
const pipesFn = async (args, ...params) => {
const resolveParamValue = async (param) => {
const { index, extractValue, type, data, metatype, pipes: paramPipes, schema, } = param;
const value = extractValue(...params);
args[index] = await this.getParamValue(value, { metatype, type, data, schema }, pipes.concat(paramPipes));
};
await Promise.all(paramsOptions.map(resolveParamValue));
};
return paramsOptions.length ? pipesFn : null;
}
async getParamValue(value, metadata, pipes) {
return isEmptyArray(pipes)
? value
: this.pipesConsumer.apply(value, metadata, pipes);
}
/**
* Returns the notifier of the context, which all the handlers called with
* it share (e.g. the event handlers of one pattern).
*/
attachHandlerObservableNotifier(context) {
if (!isObject(context)) {
return undefined;
}
let handlerObservable$ = handlerObservableNotifiers.get(context);
if (!handlerObservable$) {
handlerObservable$ = new ReplaySubject(1);
handlerObservableNotifiers.set(context, handlerObservable$);
}
return handlerObservable$;
}
notifyIfObservable(result, handlerObservable$) {
if (isObservable(result)) {
handlerObservable$.next();
}
else if (result instanceof Promise) {
// Only native promises are observed: calling `then()` on an arbitrary
// thenable (e.g. a lazy query builder) could repeat its side effects.
// This extra reaction leaves the handler's own promise chain untouched.
result.then(value => isObservable(value) && handlerObservable$.next(), () => undefined);
}
}
}