-
Notifications
You must be signed in to change notification settings - Fork 156
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit introduces command interceptors, enabling the interception of commands. Now, actions can be executed both before and after the command is handled. This feature enhances the flexibility and extensibility of command processing.
- Loading branch information
Showing
13 changed files
with
278 additions
and
6 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Injectable, Type } from '@nestjs/common'; | ||
import { ICommand, ICommandInterceptor } from './interfaces'; | ||
import { ModuleRef } from '@nestjs/core'; | ||
|
||
export type CommandInterceptorType = Type<ICommandInterceptor>; | ||
|
||
@Injectable() | ||
export class CommandInterceptionExecutor { | ||
private interceptors: ICommandInterceptor[] = []; | ||
|
||
constructor(private readonly moduleRef: ModuleRef) {} | ||
|
||
intercept<T extends ICommand, R>( | ||
command: T, | ||
next: () => Promise<R>, | ||
): Promise<R> { | ||
if (this.interceptors.length === 0) { | ||
return next(); | ||
} | ||
|
||
const nextFn = async (i = 0): Promise<R> => { | ||
if (i >= this.interceptors.length) { | ||
return next(); | ||
} | ||
|
||
const handler = () => nextFn(i + 1); | ||
return this.interceptors[i].intercept(command, handler); | ||
}; | ||
|
||
return nextFn(); | ||
} | ||
|
||
register(interceptors: CommandInterceptorType[] = []) { | ||
interceptors.forEach((interceptor) => | ||
this.registerInterceptor(interceptor), | ||
); | ||
} | ||
|
||
private registerInterceptor(interceptor: CommandInterceptorType) { | ||
const instance = this.moduleRef.get(interceptor, { strict: false }); | ||
|
||
if (!instance) { | ||
return; | ||
} | ||
|
||
this.interceptors.push(instance); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { Test } from '@nestjs/testing'; | ||
import { | ||
CqrsModule, | ||
CommandBus, | ||
ICommandHandler, | ||
ICommand, | ||
CommandHandler, | ||
CommandInterceptor, | ||
ICommandInterceptor, | ||
} from './index'; | ||
import { Type } from '@nestjs/common'; | ||
|
||
class TestCommand implements ICommand {} | ||
|
||
@CommandHandler(TestCommand) | ||
class TestCommandHandler implements ICommandHandler { | ||
execute(): Promise<any> { | ||
return Promise.resolve(undefined); | ||
} | ||
} | ||
|
||
@CommandInterceptor() | ||
class FirstCommandInterceptor implements ICommandInterceptor { | ||
intercept(_: unknown, next: () => Promise<unknown>) { | ||
return next(); | ||
} | ||
} | ||
|
||
@CommandInterceptor() | ||
class SecondCommandInterceptor implements ICommandInterceptor { | ||
intercept(_: unknown, next: () => Promise<unknown>) { | ||
return next(); | ||
} | ||
} | ||
|
||
describe('Command interception', () => { | ||
const bootstrap = async ( | ||
...interceptors: Type<ICommandInterceptor>[] | ||
): Promise<{ | ||
commandBus: CommandBus; | ||
commandHandler: TestCommandHandler; | ||
interceptors: ICommandInterceptor[]; | ||
}> => { | ||
const moduleRef = await Test.createTestingModule({ | ||
providers: [TestCommandHandler, ...interceptors], | ||
imports: [CqrsModule], | ||
}).compile(); | ||
await moduleRef.init(); | ||
|
||
return { | ||
commandBus: moduleRef.get(CommandBus), | ||
commandHandler: moduleRef.get(TestCommandHandler), | ||
interceptors: interceptors.map((interceptor) => | ||
moduleRef.get(interceptor), | ||
), | ||
}; | ||
}; | ||
|
||
it('should invoke command handler', async () => { | ||
const { commandBus, commandHandler } = await bootstrap( | ||
FirstCommandInterceptor, | ||
SecondCommandInterceptor, | ||
); | ||
|
||
const fakeResult = {}; | ||
const commandExecuteSpy = jest | ||
.spyOn(commandHandler, 'execute') | ||
.mockImplementation(() => Promise.resolve(fakeResult)); | ||
|
||
const command = new TestCommand(); | ||
const executionResult = await commandBus.execute(command); | ||
|
||
expect(commandExecuteSpy).toHaveBeenCalledWith(command); | ||
expect(executionResult).toEqual(fakeResult); | ||
}); | ||
|
||
it('should invoke every interceptor', async () => { | ||
const { | ||
commandBus, | ||
interceptors: [firstCommandInterceptor, secondCommandInterceptor], | ||
} = await bootstrap(FirstCommandInterceptor, SecondCommandInterceptor); | ||
|
||
const firstHandlerInterceptSpy = jest.spyOn( | ||
firstCommandInterceptor, | ||
'intercept', | ||
); | ||
const secondHandlerInterceptSpy = jest.spyOn( | ||
secondCommandInterceptor, | ||
'intercept', | ||
); | ||
|
||
const command = new TestCommand(); | ||
await commandBus.execute(command); | ||
|
||
expect(firstHandlerInterceptSpy).toHaveBeenCalledWith( | ||
command, | ||
expect.anything(), | ||
); | ||
expect(secondHandlerInterceptSpy).toHaveBeenCalledWith( | ||
command, | ||
expect.anything(), | ||
); | ||
}); | ||
|
||
it('should allow modification of a command', async () => { | ||
const { | ||
commandBus, | ||
interceptors: [commandInterceptor], | ||
} = await bootstrap(FirstCommandInterceptor); | ||
|
||
const fakeResult = {}; | ||
jest | ||
.spyOn(commandInterceptor, 'intercept') | ||
.mockImplementation(() => Promise.resolve(fakeResult)); | ||
|
||
const executionResult = commandBus.execute(new TestCommand()); | ||
await expect(executionResult).resolves.toEqual(fakeResult); | ||
}); | ||
|
||
it('should propagate errors and stop execution', async () => { | ||
const { | ||
commandBus, | ||
interceptors: [firstCommandInterceptor, secondCommandInterceptor], | ||
} = await bootstrap(FirstCommandInterceptor, SecondCommandInterceptor); | ||
|
||
const fakeError = new Error('FAKE_ERROR'); | ||
jest | ||
.spyOn(firstCommandInterceptor, 'intercept') | ||
.mockImplementation(() => Promise.reject(fakeError)); | ||
const secondInterceptorInterceptSpy = jest.spyOn( | ||
secondCommandInterceptor, | ||
'intercept', | ||
); | ||
|
||
await expect(commandBus.execute(new TestCommand())).rejects.toEqual( | ||
fakeError, | ||
); | ||
expect(secondInterceptorInterceptSpy).not.toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import 'reflect-metadata'; | ||
import { COMMAND_INTERCEPTOR_METADATA } from './constants'; | ||
|
||
/** | ||
* Decorator that marks a class as a Nest command interceptor. A command interceptor | ||
* intercepts commands (actions) executed by your application code and allows you to implement | ||
* cross-cutting concerns. | ||
* | ||
* The decorated class must implement the `ICommandInterceptor` interface. | ||
*/ | ||
export const CommandInterceptor = (): ClassDecorator => { | ||
return (target: object) => { | ||
Reflect.defineMetadata(COMMAND_INTERCEPTOR_METADATA, {}, target); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { ICommand } from './command.interface'; | ||
|
||
/** | ||
* Interface describing implementation of a command interceptor | ||
* | ||
* @publicApi | ||
*/ | ||
export interface ICommandInterceptor<T extends ICommand = any, R = any> { | ||
/** | ||
* Method to implement a custom command interceptor. | ||
* @param command the command to execute. | ||
* @param next a reference to the function, which provides access to the command handler | ||
*/ | ||
intercept(command: T, next: () => Promise<R>): Promise<R>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.