mirror of
https://github.com/nestjs/nest.git
synced 2026-02-25 20:25:50 +00:00
Merge pull request #16429 from suuuuuuminnnnnn/feat/microservices-pre-request-hook-v12
Feat/microservices pre request hook v12
This commit is contained in:
@@ -41,6 +41,7 @@ export {
|
||||
NestHybridApplicationOptions,
|
||||
NestInterceptor,
|
||||
NestMiddleware,
|
||||
PreRequestHook,
|
||||
NestModule,
|
||||
OnApplicationBootstrap,
|
||||
OnApplicationShutdown,
|
||||
|
||||
@@ -17,6 +17,7 @@ export * from './hooks/index.js';
|
||||
export * from './http/index.js';
|
||||
export * from './injectable.interface.js';
|
||||
export * from './microservices/nest-hybrid-application-options.interface.js';
|
||||
export * from './microservices/pre-request-hook.interface.js';
|
||||
export * from './middleware/index.js';
|
||||
export * from './modules/index.js';
|
||||
export * from './nest-application-context.interface.js';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { ExecutionContext } from '../features/execution-context.interface.js';
|
||||
|
||||
/**
|
||||
* Interface describing a global preRequest hook for microservices.
|
||||
*
|
||||
* Hooks are executed before guards, allowing setup of context (e.g. AsyncLocalStorage)
|
||||
* that is available to all downstream enhancers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const als = new AsyncLocalStorage();
|
||||
* app.registerPreRequestHook((context, next) => {
|
||||
* als.enterWith({ correlationId: uuid() });
|
||||
* return next();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface PreRequestHook {
|
||||
(
|
||||
context: ExecutionContext,
|
||||
next: () => Observable<unknown>,
|
||||
): Observable<unknown>;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ExceptionFilter } from './exceptions/exception-filter.interface.js';
|
||||
import { CanActivate } from './features/can-activate.interface.js';
|
||||
import { NestInterceptor } from './features/nest-interceptor.interface.js';
|
||||
import { PipeTransform } from './features/pipe-transform.interface.js';
|
||||
import { PreRequestHook } from './microservices/pre-request-hook.interface.js';
|
||||
import { INestApplicationContext } from './nest-application-context.interface.js';
|
||||
import { WebSocketAdapter } from './websockets/web-socket-adapter.interface.js';
|
||||
|
||||
@@ -56,6 +57,15 @@ export interface INestMicroservice extends INestApplicationContext {
|
||||
*/
|
||||
useGlobalGuards(...guards: CanActivate[]): this;
|
||||
|
||||
/**
|
||||
* Registers a global preRequest hook (executed before all enhancers for every pattern handler).
|
||||
* Hooks receive an `ExecutionContext` and a `next` function that executes the rest of the pipeline.
|
||||
* Useful for setting up AsyncLocalStorage context, tracing, or correlation IDs.
|
||||
*
|
||||
* @param {...PreRequestHook} hooks
|
||||
*/
|
||||
registerPreRequestHook(...hooks: PreRequestHook[]): this;
|
||||
|
||||
/**
|
||||
* Terminates the application.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ExceptionFilter,
|
||||
NestInterceptor,
|
||||
PipeTransform,
|
||||
PreRequestHook,
|
||||
VersioningOptions,
|
||||
WebSocketAdapter,
|
||||
} from '@nestjs/common';
|
||||
@@ -17,6 +18,7 @@ export class ApplicationConfig {
|
||||
private globalFilters: Array<ExceptionFilter> = [];
|
||||
private globalInterceptors: Array<NestInterceptor> = [];
|
||||
private globalGuards: Array<CanActivate> = [];
|
||||
private globalPreRequestHooks: Array<PreRequestHook> = [];
|
||||
private versioningOptions: VersioningOptions;
|
||||
private readonly globalRequestPipes: InstanceWrapper<PipeTransform>[] = [];
|
||||
private readonly globalRequestFilters: InstanceWrapper<ExceptionFilter>[] =
|
||||
@@ -135,6 +137,14 @@ export class ApplicationConfig {
|
||||
return this.globalRequestGuards;
|
||||
}
|
||||
|
||||
public registerPreRequestHook(...hooks: PreRequestHook[]) {
|
||||
this.globalPreRequestHooks = this.globalPreRequestHooks.concat(hooks);
|
||||
}
|
||||
|
||||
public getGlobalPreRequestHooks(): PreRequestHook[] {
|
||||
return this.globalPreRequestHooks;
|
||||
}
|
||||
|
||||
public enableVersioning(options: VersioningOptions): void {
|
||||
if (Array.isArray(options.defaultVersion)) {
|
||||
// Drop duplicated versions
|
||||
|
||||
@@ -105,6 +105,22 @@ describe('ApplicationConfig', () => {
|
||||
expect(appConfig.getGlobalRequestGuards()).toContain(guard);
|
||||
});
|
||||
});
|
||||
describe('PreRequestHooks', () => {
|
||||
it('should set global preRequest hooks', () => {
|
||||
const hooks = [() => {}, () => {}];
|
||||
appConfig.registerPreRequestHook(...(hooks as any));
|
||||
|
||||
expect(appConfig.getGlobalPreRequestHooks()).toEqual(hooks);
|
||||
});
|
||||
it('should accumulate multiple registerPreRequestHook calls', () => {
|
||||
const hook1 = () => {};
|
||||
const hook2 = () => {};
|
||||
appConfig.registerPreRequestHook(hook1 as any);
|
||||
appConfig.registerPreRequestHook(hook2 as any);
|
||||
|
||||
expect(appConfig.getGlobalPreRequestHooks()).toEqual([hook1, hook2]);
|
||||
});
|
||||
});
|
||||
describe('Interceptors', () => {
|
||||
it('should set global interceptors', () => {
|
||||
const interceptors = ['test', 'test2'];
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { ContextType, PipeTransform } from '@nestjs/common';
|
||||
import type {
|
||||
ContextType,
|
||||
PipeTransform,
|
||||
PreRequestHook,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
type Controller,
|
||||
CUSTOM_ROUTE_ARGS_METADATA,
|
||||
isEmptyArray,
|
||||
PARAMTYPES_METADATA,
|
||||
} from '@nestjs/common/internal';
|
||||
import type { ApplicationConfig } from '@nestjs/core';
|
||||
import {
|
||||
ContextUtils,
|
||||
type ExecutionContextHost,
|
||||
ExecutionContextHost,
|
||||
FORBIDDEN_MESSAGE,
|
||||
type GuardsConsumer,
|
||||
type GuardsContextCreator,
|
||||
@@ -20,7 +25,7 @@ import {
|
||||
type PipesContextCreator,
|
||||
STATIC_CONTEXT,
|
||||
} from '@nestjs/core/internal';
|
||||
import { Observable } from 'rxjs';
|
||||
import { defer, from, mergeMap, Observable } from 'rxjs';
|
||||
import { PARAM_ARGS_METADATA } from '../constants.js';
|
||||
import { RpcException } from '../exceptions/index.js';
|
||||
import { RpcParamsFactory } from '../factories/rpc-params-factory.js';
|
||||
@@ -50,6 +55,7 @@ export class RpcContextCreator {
|
||||
private readonly guardsConsumer: GuardsConsumer,
|
||||
private readonly interceptorsContextCreator: InterceptorsContextCreator,
|
||||
private readonly interceptorsConsumer: InterceptorsConsumer,
|
||||
private readonly applicationConfig?: ApplicationConfig,
|
||||
) {}
|
||||
|
||||
public create<T extends ParamsMetadata = ParamsMetadata>(
|
||||
@@ -119,18 +125,46 @@ export class RpcContextCreator {
|
||||
return callback.apply(instance, args);
|
||||
};
|
||||
|
||||
const preRequestHooks =
|
||||
this.applicationConfig?.getGlobalPreRequestHooks() ?? [];
|
||||
|
||||
return this.rpcProxy.create(async (...args: unknown[]) => {
|
||||
const initialArgs = this.contextUtils.createNullArray(argsLength);
|
||||
fnCanActivate && (await fnCanActivate(args));
|
||||
|
||||
return this.interceptorsConsumer.intercept(
|
||||
interceptors,
|
||||
const executePipeline = async () => {
|
||||
fnCanActivate && (await fnCanActivate(args));
|
||||
return this.interceptorsConsumer.intercept(
|
||||
interceptors,
|
||||
args,
|
||||
instance,
|
||||
callback,
|
||||
handler(initialArgs, args),
|
||||
contextType,
|
||||
) as Promise<Observable<unknown>>;
|
||||
};
|
||||
|
||||
if (preRequestHooks.length === 0) {
|
||||
return executePipeline();
|
||||
}
|
||||
|
||||
const executionContext = new ExecutionContextHost(
|
||||
args,
|
||||
instance,
|
||||
instance.constructor as any,
|
||||
callback,
|
||||
handler(initialArgs, args),
|
||||
contextType,
|
||||
) as Promise<Observable<unknown>>;
|
||||
);
|
||||
executionContext.setType(contextType);
|
||||
|
||||
const pipelineObs: Observable<unknown> = defer(() =>
|
||||
from(executePipeline()).pipe(mergeMap(obs => obs)),
|
||||
);
|
||||
|
||||
let index = 0;
|
||||
const next = (): Observable<unknown> => {
|
||||
if (index >= preRequestHooks.length) return pipelineObs;
|
||||
return preRequestHooks[index++](executionContext, next);
|
||||
};
|
||||
|
||||
return next();
|
||||
}, exceptionHandler);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export class MicroservicesModule<
|
||||
new GuardsConsumer(),
|
||||
new InterceptorsContextCreator(container, config),
|
||||
new InterceptorsConsumer(),
|
||||
config,
|
||||
);
|
||||
|
||||
const injector = new Injector({
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
INestMicroservice,
|
||||
NestInterceptor,
|
||||
PipeTransform,
|
||||
PreRequestHook,
|
||||
WebSocketAdapter,
|
||||
} from '@nestjs/common';
|
||||
import { Transport } from './enums/transport.enum.js';
|
||||
@@ -247,6 +248,21 @@ export class NestMicroservice
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a global preRequest hook (executed before all enhancers for every pattern handler).
|
||||
*
|
||||
* @param {...PreRequestHook} hooks
|
||||
*/
|
||||
public registerPreRequestHook(...hooks: PreRequestHook[]): this {
|
||||
if (this.isInitialized) {
|
||||
this.logger.warn(
|
||||
'Cannot apply global preRequest hooks: registration must occur before initialization.',
|
||||
);
|
||||
}
|
||||
this.applicationConfig.registerPreRequestHook(...hooks);
|
||||
return this;
|
||||
}
|
||||
|
||||
public async init(): Promise<this> {
|
||||
if (this.isInitialized) {
|
||||
return this;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host.js';
|
||||
import { of } from 'rxjs';
|
||||
import { CUSTOM_ROUTE_ARGS_METADATA } from '../../../common/constants.js';
|
||||
import { Injectable, UseGuards, UsePipes } from '../../../common/index.js';
|
||||
import { ApplicationConfig } from '../../../core/application-config.js';
|
||||
import { GuardsConsumer } from '../../../core/guards/guards-consumer.js';
|
||||
import { GuardsContextCreator } from '../../../core/guards/guards-context-creator.js';
|
||||
import { NestContainer } from '../../../core/injector/container.js';
|
||||
import { InterceptorsConsumer } from '../../../core/interceptors/interceptors-consumer.js';
|
||||
import { InterceptorsContextCreator } from '../../../core/interceptors/interceptors-context-creator.js';
|
||||
import { PipesConsumer } from '../../../core/pipes/pipes-consumer.js';
|
||||
import { PipesContextCreator } from '../../../core/pipes/pipes-context-creator.js';
|
||||
import { ExceptionFiltersContext } from '../../context/exception-filters-context.js';
|
||||
import { RpcContextCreator } from '../../context/rpc-context-creator.js';
|
||||
import { RpcProxy } from '../../context/rpc-proxy.js';
|
||||
import { RpcParamtype } from '../../enums/rpc-paramtype.enum.js';
|
||||
import { RpcParamsFactory } from '../../factories/rpc-params-factory.js';
|
||||
import { RpcException } from '../../index.js';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { Injectable, UseGuards, UsePipes } from '../../../common';
|
||||
import { CUSTOM_ROUTE_ARGS_METADATA } from '../../../common/constants';
|
||||
import { ApplicationConfig } from '../../../core/application-config';
|
||||
import { GuardsConsumer } from '../../../core/guards/guards-consumer';
|
||||
import { GuardsContextCreator } from '../../../core/guards/guards-context-creator';
|
||||
import { NestContainer } from '../../../core/injector/container';
|
||||
import { InterceptorsConsumer } from '../../../core/interceptors/interceptors-consumer';
|
||||
import { InterceptorsContextCreator } from '../../../core/interceptors/interceptors-context-creator';
|
||||
import { PipesConsumer } from '../../../core/pipes/pipes-consumer';
|
||||
import { PipesContextCreator } from '../../../core/pipes/pipes-context-creator';
|
||||
import { ExceptionFiltersContext } from '../../context/exception-filters-context';
|
||||
import { RpcContextCreator } from '../../context/rpc-context-creator';
|
||||
import { RpcProxy } from '../../context/rpc-proxy';
|
||||
import { RpcParamtype } from '../../enums/rpc-paramtype.enum';
|
||||
import { RpcParamsFactory } from '../../factories/rpc-params-factory';
|
||||
import { RpcException } from '../../index';
|
||||
|
||||
@Injectable()
|
||||
class TestGuard {
|
||||
@@ -76,6 +76,11 @@ describe('RpcContextCreator', () => {
|
||||
instance = new Test();
|
||||
module = 'test';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create exception handler', () => {
|
||||
const handlerCreateSpy = vi.spyOn(exceptionFiltersContext, 'create');
|
||||
@@ -199,7 +204,7 @@ describe('RpcContextCreator', () => {
|
||||
});
|
||||
});
|
||||
describe('getParamValue', () => {
|
||||
let consumerApplySpy: ReturnType<typeof vi.fn>;
|
||||
let consumerApplySpy: any;
|
||||
const value = 3,
|
||||
metatype = null,
|
||||
transforms = [{ transform: vi.fn() }];
|
||||
@@ -242,4 +247,280 @@ describe('RpcContextCreator', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('preRequest hooks', () => {
|
||||
function makeCreatorWithHooks(hooks: any[]) {
|
||||
const container: any = new NestContainer();
|
||||
const localRpcProxy = new RpcProxy();
|
||||
const localExceptionFiltersContext = new ExceptionFiltersContext(
|
||||
container,
|
||||
new ApplicationConfig() as any,
|
||||
);
|
||||
vi.spyOn(localRpcProxy, 'create').mockImplementation(a => a);
|
||||
|
||||
const mockConfig = {
|
||||
getGlobalPreRequestHooks: () => hooks,
|
||||
} as any;
|
||||
|
||||
return new RpcContextCreator(
|
||||
localRpcProxy,
|
||||
localExceptionFiltersContext,
|
||||
new PipesContextCreator(container) as any,
|
||||
new PipesConsumer() as any,
|
||||
new GuardsContextCreator(container) as any,
|
||||
new GuardsConsumer() as any,
|
||||
new InterceptorsContextCreator(container) as any,
|
||||
new InterceptorsConsumer() as any,
|
||||
mockConfig,
|
||||
);
|
||||
}
|
||||
|
||||
it('should execute preRequest hook before guards', async () => {
|
||||
const executionOrder: string[] = [];
|
||||
|
||||
const hookFn = (_ctx: any, next: () => Observable<unknown>) => {
|
||||
executionOrder.push('hook');
|
||||
return next();
|
||||
};
|
||||
|
||||
const creator = makeCreatorWithHooks([hookFn]);
|
||||
vi.spyOn(
|
||||
new GuardsContextCreator(new NestContainer() as any),
|
||||
'create',
|
||||
).mockReturnValue([] as any);
|
||||
|
||||
const localGuardsConsumer = new GuardsConsumer();
|
||||
vi.spyOn(localGuardsConsumer, 'tryActivate').mockImplementation(
|
||||
async () => {
|
||||
executionOrder.push('guard');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
const container: any = new NestContainer();
|
||||
const localRpcProxy = new RpcProxy();
|
||||
const localExceptionFiltersContext = new ExceptionFiltersContext(
|
||||
container,
|
||||
new ApplicationConfig() as any,
|
||||
);
|
||||
vi.spyOn(localRpcProxy, 'create').mockImplementation(a => a);
|
||||
const mockConfig = { getGlobalPreRequestHooks: () => [hookFn] } as any;
|
||||
const localGuardsContextCreator = new GuardsContextCreator(container);
|
||||
vi.spyOn(localGuardsContextCreator, 'create').mockReturnValue([
|
||||
{
|
||||
canActivate: () => {
|
||||
executionOrder.push('guard');
|
||||
return true;
|
||||
},
|
||||
},
|
||||
] as any);
|
||||
|
||||
const hookCreator = new RpcContextCreator(
|
||||
localRpcProxy,
|
||||
localExceptionFiltersContext,
|
||||
new PipesContextCreator(container) as any,
|
||||
new PipesConsumer() as any,
|
||||
localGuardsContextCreator as any,
|
||||
new GuardsConsumer() as any,
|
||||
new InterceptorsContextCreator(container) as any,
|
||||
new InterceptorsConsumer() as any,
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
const proxy = hookCreator.create(instance, instance.test, module, 'test');
|
||||
const result = await proxy('data');
|
||||
if (result && typeof (result as any).subscribe === 'function') {
|
||||
await new Promise<void>(resolve => {
|
||||
(result as Observable<unknown>).subscribe({
|
||||
complete: resolve,
|
||||
error: resolve,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
expect(executionOrder[0]).toBe('hook');
|
||||
expect(executionOrder[1]).toBe('guard');
|
||||
});
|
||||
|
||||
it('should chain multiple hooks in registration order', async () => {
|
||||
const order: string[] = [];
|
||||
|
||||
const hook1 = (_ctx: any, next: () => Observable<unknown>) => {
|
||||
order.push('hook1');
|
||||
return next();
|
||||
};
|
||||
const hook2 = (_ctx: any, next: () => Observable<unknown>) => {
|
||||
order.push('hook2');
|
||||
return next();
|
||||
};
|
||||
|
||||
const container: any = new NestContainer();
|
||||
const localRpcProxy = new RpcProxy();
|
||||
vi.spyOn(localRpcProxy, 'create').mockImplementation(a => a);
|
||||
const mockConfig = {
|
||||
getGlobalPreRequestHooks: () => [hook1, hook2],
|
||||
} as any;
|
||||
const localGuardsContextCreator = new GuardsContextCreator(container);
|
||||
vi.spyOn(localGuardsContextCreator, 'create').mockReturnValue([
|
||||
{
|
||||
canActivate: () => {
|
||||
order.push('guard');
|
||||
return true;
|
||||
},
|
||||
},
|
||||
] as any);
|
||||
|
||||
const hookCreator = new RpcContextCreator(
|
||||
localRpcProxy,
|
||||
new ExceptionFiltersContext(container, new ApplicationConfig() as any),
|
||||
new PipesContextCreator(container) as any,
|
||||
new PipesConsumer() as any,
|
||||
localGuardsContextCreator as any,
|
||||
new GuardsConsumer() as any,
|
||||
new InterceptorsContextCreator(container) as any,
|
||||
new InterceptorsConsumer() as any,
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
const proxy = hookCreator.create(instance, instance.test, module, 'test');
|
||||
const result = await proxy('data');
|
||||
if (result && typeof (result as any).subscribe === 'function') {
|
||||
await new Promise<void>(resolve => {
|
||||
(result as Observable<unknown>).subscribe({
|
||||
complete: resolve,
|
||||
error: resolve,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
expect(order).toEqual(['hook1', 'hook2', 'guard']);
|
||||
});
|
||||
|
||||
it('should not call hook when no hooks are registered (fast-path)', async () => {
|
||||
const hookFn = vi.fn((_ctx: any, next: () => Observable<unknown>) =>
|
||||
next(),
|
||||
);
|
||||
const creator = makeCreatorWithHooks([]);
|
||||
|
||||
const guardSpy = vi.spyOn(guardsConsumer, 'tryActivate');
|
||||
vi.spyOn(guardsContextCreator, 'create').mockImplementation(
|
||||
() => [{ canActivate: () => true }] as any,
|
||||
);
|
||||
|
||||
contextCreator = new RpcContextCreator(
|
||||
rpcProxy,
|
||||
exceptionFiltersContext,
|
||||
pipesCreator as any,
|
||||
pipesConsumer as any,
|
||||
guardsContextCreator as any,
|
||||
guardsConsumer as any,
|
||||
new InterceptorsContextCreator(new NestContainer() as any) as any,
|
||||
new InterceptorsConsumer() as any,
|
||||
{ getGlobalPreRequestHooks: () => [] } as any,
|
||||
);
|
||||
|
||||
const proxy = contextCreator.create(
|
||||
instance,
|
||||
instance.test,
|
||||
module,
|
||||
'test',
|
||||
);
|
||||
await proxy('data');
|
||||
expect(hookFn).not.toHaveBeenCalled();
|
||||
expect(guardSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should provide ExecutionContext with getClass() and getHandler() to the hook', async () => {
|
||||
let capturedContext: any;
|
||||
|
||||
const hookFn = (ctx: any, next: () => Observable<unknown>) => {
|
||||
capturedContext = ctx;
|
||||
return next();
|
||||
};
|
||||
|
||||
const container: any = new NestContainer();
|
||||
const localRpcProxy = new RpcProxy();
|
||||
vi.spyOn(localRpcProxy, 'create').mockImplementation(a => a);
|
||||
const mockConfig = { getGlobalPreRequestHooks: () => [hookFn] } as any;
|
||||
const localGuardsContextCreator = new GuardsContextCreator(container);
|
||||
vi.spyOn(localGuardsContextCreator, 'create').mockReturnValue([] as any);
|
||||
|
||||
const hookCreator = new RpcContextCreator(
|
||||
localRpcProxy,
|
||||
new ExceptionFiltersContext(container, new ApplicationConfig() as any),
|
||||
new PipesContextCreator(container) as any,
|
||||
new PipesConsumer() as any,
|
||||
localGuardsContextCreator as any,
|
||||
new GuardsConsumer() as any,
|
||||
new InterceptorsContextCreator(container) as any,
|
||||
new InterceptorsConsumer() as any,
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
const proxy = hookCreator.create(instance, instance.test, module, 'test');
|
||||
const result = await proxy('data');
|
||||
if (result && typeof (result as any).subscribe === 'function') {
|
||||
await new Promise<void>(resolve => {
|
||||
(result as Observable<unknown>).subscribe({
|
||||
complete: resolve,
|
||||
error: resolve,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
expect(capturedContext).not.toBeUndefined();
|
||||
expect(capturedContext.getClass()).toBe(Test);
|
||||
expect(capturedContext.getHandler()).toBe(instance.test);
|
||||
expect(capturedContext.getType()).toBe('rpc');
|
||||
});
|
||||
|
||||
it('should simulate ALS context available in guard (AsyncLocalStorage scenario)', async () => {
|
||||
const store = new Map<string, string>();
|
||||
let correlationIdInGuard: string | undefined;
|
||||
|
||||
const hookFn = (_ctx: any, next: () => Observable<unknown>) => {
|
||||
store.set('correlationId', 'test-id-123');
|
||||
return next();
|
||||
};
|
||||
|
||||
const container: any = new NestContainer();
|
||||
const localRpcProxy = new RpcProxy();
|
||||
vi.spyOn(localRpcProxy, 'create').mockImplementation(a => a);
|
||||
const mockConfig = { getGlobalPreRequestHooks: () => [hookFn] } as any;
|
||||
const localGuardsContextCreator = new GuardsContextCreator(container);
|
||||
vi.spyOn(localGuardsContextCreator, 'create').mockReturnValue([
|
||||
{
|
||||
canActivate: () => {
|
||||
correlationIdInGuard = store.get('correlationId');
|
||||
return true;
|
||||
},
|
||||
},
|
||||
] as any);
|
||||
|
||||
const hookCreator = new RpcContextCreator(
|
||||
localRpcProxy,
|
||||
new ExceptionFiltersContext(container, new ApplicationConfig() as any),
|
||||
new PipesContextCreator(container) as any,
|
||||
new PipesConsumer() as any,
|
||||
localGuardsContextCreator as any,
|
||||
new GuardsConsumer() as any,
|
||||
new InterceptorsContextCreator(container) as any,
|
||||
new InterceptorsConsumer() as any,
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
const proxy = hookCreator.create(instance, instance.test, module, 'test');
|
||||
const result = await proxy('data');
|
||||
if (result && typeof (result as any).subscribe === 'function') {
|
||||
await new Promise<void>(resolve => {
|
||||
(result as Observable<unknown>).subscribe({
|
||||
complete: resolve,
|
||||
error: resolve,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
expect(correlationIdInGuard).toBe('test-id-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,4 +152,66 @@ describe('NestMicroservice', () => {
|
||||
instance.on('test:event', cb);
|
||||
expect(onStub).toHaveBeenCalledWith('test:event', cb);
|
||||
});
|
||||
|
||||
describe('registerPreRequestHook', () => {
|
||||
it('should delegate to applicationConfig.registerPreRequestHook', () => {
|
||||
const mockConfig = {
|
||||
...createMockAppConfig(),
|
||||
registerPreRequestHook: vi.fn(),
|
||||
} as unknown as ApplicationConfig;
|
||||
|
||||
const instance = new NestMicroservice(
|
||||
mockContainer,
|
||||
{ transport: Transport.TCP },
|
||||
mockGraphInspector,
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
const hook = (_ctx: any, next: any) => next();
|
||||
instance.registerPreRequestHook(hook);
|
||||
|
||||
expect(mockConfig.registerPreRequestHook).toHaveBeenCalledWith(hook);
|
||||
});
|
||||
|
||||
it('should warn when called after initialization', () => {
|
||||
const mockConfig = {
|
||||
...createMockAppConfig(),
|
||||
registerPreRequestHook: vi.fn(),
|
||||
} as unknown as ApplicationConfig;
|
||||
|
||||
const instance = new NestMicroservice(
|
||||
mockContainer,
|
||||
{ transport: Transport.TCP },
|
||||
mockGraphInspector,
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn((instance as any).logger, 'warn');
|
||||
(instance as any).isInitialized = true;
|
||||
|
||||
const hook = (_ctx: any, next: any) => next();
|
||||
instance.registerPreRequestHook(hook);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return this for fluent API chaining', () => {
|
||||
const mockConfig = {
|
||||
...createMockAppConfig(),
|
||||
registerPreRequestHook: vi.fn(),
|
||||
} as unknown as ApplicationConfig;
|
||||
|
||||
const instance = new NestMicroservice(
|
||||
mockContainer,
|
||||
{ transport: Transport.TCP },
|
||||
mockGraphInspector,
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
const hook = (_ctx: any, next: any) => next();
|
||||
const result = instance.registerPreRequestHook(hook);
|
||||
|
||||
expect(result).toBe(instance);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user