Skip to content

Commit

Permalink
feat(core): add simple query bus
Browse files Browse the repository at this point in the history
  • Loading branch information
gtoselli committed Apr 8, 2024
1 parent 924e5c0 commit ce9d5a5
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/ddd-toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Check warning on line 8 in packages/ddd-toolkit/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/ddd-toolkit/src/index.ts#L8

Added line #L8 was not covered by tests
3 changes: 3 additions & 0 deletions packages/ddd-toolkit/src/query-bus/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './query';
export * from './query-bus.interface';
export * from './local-query-bus';

Check warning on line 3 in packages/ddd-toolkit/src/query-bus/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/ddd-toolkit/src/query-bus/index.ts#L1-L3

Added lines #L1 - L3 were not covered by tests
121 changes: 121 additions & 0 deletions packages/ddd-toolkit/src/query-bus/local-query-bus.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
});
});
20 changes: 20 additions & 0 deletions packages/ddd-toolkit/src/query-bus/local-query-bus.ts
Original file line number Diff line number Diff line change
@@ -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<IQuery<unknown, unknown>> } = {};

constructor(private logger: ILogger) {}

public register<Q extends IQuery<unknown, unknown>>(query: IQueryClass<Q>, handler: IQueryHandler<Q>): 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<Q extends IQuery<unknown, unknown>>(query: Q): Promise<Q['_resultType']> {
const handler = this.handlers[query.name] as IQueryHandler<Q>;
if (!handler) throw new Error(`No handler found for ${query.name}`);
return await handler.handle(query);
}
}
19 changes: 19 additions & 0 deletions packages/ddd-toolkit/src/query-bus/query-bus.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface IQuery<TPayload, TResult = void> {
name: string;
payload: TPayload;
_resultType: TResult;
}

export interface IQueryClass<Q extends IQuery<unknown, unknown>> {
new (payload: unknown): Q;
}

export interface IQueryHandler<Q extends IQuery<unknown, unknown>> {
handle: (query: Q) => Promise<void>;
}

export interface IQueryBus {
register<Q extends IQuery<unknown, unknown>>(query: IQueryClass<Q>, handler: IQueryHandler<Q>): void;

execute<Q extends IQuery<unknown, unknown>>(query: Q): Promise<Q['_resultType']>;
}
10 changes: 10 additions & 0 deletions packages/ddd-toolkit/src/query-bus/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IQuery } from './query-bus.interface';

export abstract class Query<TPayload, TResult = void> implements IQuery<TPayload, TResult> {
readonly name: string;
readonly _resultType: TResult;

protected constructor(public readonly payload: TPayload) {
this.name = this.constructor.name;
}
}

0 comments on commit ce9d5a5

Please sign in to comment.