From ce9d5a5d73545706d670eb9029afe551735228ff Mon Sep 17 00:00:00 2001 From: Gabriele Toselli Date: Mon, 8 Apr 2024 12:34:42 +0200 Subject: [PATCH] feat(core): add simple query bus --- packages/ddd-toolkit/src/index.ts | 1 + packages/ddd-toolkit/src/query-bus/index.ts | 3 + .../src/query-bus/local-query-bus.spec.ts | 121 ++++++++++++++++++ .../src/query-bus/local-query-bus.ts | 20 +++ .../src/query-bus/query-bus.interface.ts | 19 +++ packages/ddd-toolkit/src/query-bus/query.ts | 10 ++ 6 files changed, 174 insertions(+) create mode 100644 packages/ddd-toolkit/src/query-bus/index.ts create mode 100644 packages/ddd-toolkit/src/query-bus/local-query-bus.spec.ts create mode 100644 packages/ddd-toolkit/src/query-bus/local-query-bus.ts create mode 100644 packages/ddd-toolkit/src/query-bus/query-bus.interface.ts create mode 100644 packages/ddd-toolkit/src/query-bus/query.ts diff --git a/packages/ddd-toolkit/src/index.ts b/packages/ddd-toolkit/src/index.ts index 6afc49b..533623b 100644 --- a/packages/ddd-toolkit/src/index.ts +++ b/packages/ddd-toolkit/src/index.ts @@ -5,3 +5,4 @@ export * from './errors'; export * from './repo/mongo-query-repo'; export * from './event-bus/event-bus.interface'; export * from './repo/mongo-aggregate-repo-with-outbox'; +export * from './query-bus'; diff --git a/packages/ddd-toolkit/src/query-bus/index.ts b/packages/ddd-toolkit/src/query-bus/index.ts new file mode 100644 index 0000000..33a7181 --- /dev/null +++ b/packages/ddd-toolkit/src/query-bus/index.ts @@ -0,0 +1,3 @@ +export * from './query'; +export * from './query-bus.interface'; +export * from './local-query-bus'; diff --git a/packages/ddd-toolkit/src/query-bus/local-query-bus.spec.ts b/packages/ddd-toolkit/src/query-bus/local-query-bus.spec.ts new file mode 100644 index 0000000..9c07871 --- /dev/null +++ b/packages/ddd-toolkit/src/query-bus/local-query-bus.spec.ts @@ -0,0 +1,121 @@ +import { LocalQueryBus } from './local-query-bus'; +import { Query } from './query'; +import { loggerMock } from '../logger'; +import { waitFor } from '../utils'; + +class FooQuery extends Query<{ foo: string }> { + constructor(public readonly payload: { foo: string }) { + super(payload); + } +} + +class BarQuery extends Query<{ foo: string }> { + constructor(public readonly payload: { foo: string }) { + super(payload); + } +} + +describe('LocalQueryBus', () => { + describe('Given a query bus', () => { + let queryBus: LocalQueryBus; + + beforeEach(() => { + queryBus = new LocalQueryBus(loggerMock); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('Given no registered handler to foo query', () => { + describe('When execute a foo query', () => { + it('should throw', async () => { + const query = new FooQuery({ foo: 'bar' }); + await expect(() => queryBus.execute(query)).rejects.toThrow( + `No handler found for ${FooQuery.name}`, + ); + }); + }); + }); + + describe('Given one registered handler to foo query', () => { + const handler1Mock = jest.fn(); + + class FooQueryHandler { + async handle(query: FooQuery) { + await handler1Mock(query); + } + } + + beforeEach(() => { + queryBus.register(FooQuery, new FooQueryHandler()); + }); + + describe('When execute a foo query', () => { + it('Should call handler', async () => { + const query = new FooQuery({ foo: 'bar' }); + await queryBus.execute(query); + + await waitFor(() => expect(handler1Mock).toBeCalledWith(query)); + }); + }); + + describe('Given a handler registered for bar query', () => { + const handler3Mock = jest.fn(); + + class BarQueryHandler { + async handle(query: BarQuery) { + await handler3Mock(query); + } + } + + beforeEach(() => { + queryBus.register(BarQuery, new BarQueryHandler()); + }); + + describe('When execute foo query', () => { + it('Should call only foo query handler', async () => { + const query = new FooQuery({ foo: 'bar' }); + await queryBus.execute(query); + + await waitFor(() => expect(handler1Mock).toBeCalledWith(query)); + expect(handler3Mock).not.toBeCalled(); + }); + }); + + describe('When send bar query', () => { + it('Should call only bar query handler', async () => { + const query = new BarQuery({ foo: 'bar' }); + await queryBus.execute(query); + + expect(handler1Mock).not.toBeCalled(); + expect(handler3Mock).toBeCalledWith(query); + }); + }); + }); + }); + + describe('Given one registered handler which fails the execution', () => { + const handlerMock = jest.fn(); + + class FooQueryHandler { + async handle(query: FooQuery) { + await handlerMock(query); + } + } + + beforeEach(() => { + handlerMock.mockRejectedValueOnce(new Error('ko')); + queryBus.register(FooQuery, new FooQueryHandler()); + }); + + describe('When execute query', () => { + const query = new FooQuery({ foo: 'bar' }); + + it('should throw', async () => { + await expect(() => queryBus.execute(query)).rejects.toThrow('ko'); + }); + }); + }); + }); +}); diff --git a/packages/ddd-toolkit/src/query-bus/local-query-bus.ts b/packages/ddd-toolkit/src/query-bus/local-query-bus.ts new file mode 100644 index 0000000..4e84f1e --- /dev/null +++ b/packages/ddd-toolkit/src/query-bus/local-query-bus.ts @@ -0,0 +1,20 @@ +import { ILogger } from '../logger'; +import { IQuery, IQueryBus, IQueryClass, IQueryHandler } from './query-bus.interface'; + +export class LocalQueryBus implements IQueryBus { + private handlers: { [key: string]: IQueryHandler> } = {}; + + constructor(private logger: ILogger) {} + + public register>(query: IQueryClass, handler: IQueryHandler): void { + if (this.handlers[query.name]) throw new Error(`Query ${query.name} is already registered`); + this.handlers[query.name] = handler; + this.logger.debug(`Query ${query.name} registered`); + } + + public async execute>(query: Q): Promise { + const handler = this.handlers[query.name] as IQueryHandler; + if (!handler) throw new Error(`No handler found for ${query.name}`); + return await handler.handle(query); + } +} diff --git a/packages/ddd-toolkit/src/query-bus/query-bus.interface.ts b/packages/ddd-toolkit/src/query-bus/query-bus.interface.ts new file mode 100644 index 0000000..ea0cb77 --- /dev/null +++ b/packages/ddd-toolkit/src/query-bus/query-bus.interface.ts @@ -0,0 +1,19 @@ +export interface IQuery { + name: string; + payload: TPayload; + _resultType: TResult; +} + +export interface IQueryClass> { + new (payload: unknown): Q; +} + +export interface IQueryHandler> { + handle: (query: Q) => Promise; +} + +export interface IQueryBus { + register>(query: IQueryClass, handler: IQueryHandler): void; + + execute>(query: Q): Promise; +} diff --git a/packages/ddd-toolkit/src/query-bus/query.ts b/packages/ddd-toolkit/src/query-bus/query.ts new file mode 100644 index 0000000..4a9d870 --- /dev/null +++ b/packages/ddd-toolkit/src/query-bus/query.ts @@ -0,0 +1,10 @@ +import { IQuery } from './query-bus.interface'; + +export abstract class Query implements IQuery { + readonly name: string; + readonly _resultType: TResult; + + protected constructor(public readonly payload: TPayload) { + this.name = this.constructor.name; + } +}