CQRS example

Try it
Start your own copy of this example:
npm create @setu-ts my-app -- --example cqrs
cd my-app && npm install
npm start
The commands further down run inside a clone of the repository.

Uses CqrsPlugin to put a note through a command handler and read it through a separate query handler.

pnpm --dir apps/cqrs run start
pnpm --dir apps/cqrs run smoke

Read the example deep dive for the concept and source walkthrough.

Read, in order

src/modules/notes/index.ts

import { CAPABILITIES } from '@setu-ts/common';
import type { ICommandBus, IQueryBus, IServiceRegistry } from '@setu-ts/common';
import type { CqrsPluginOptions } from '@setu-ts/cqrs-plugin';
import { ADD_NOTE, addNoteHandler } from './notes.commands.ts';
import type { AddNote } from './notes.commands.ts';
import { READ_NOTES, readNotesHandler } from './notes.queries.ts';
import type { ReadNotes } from './notes.queries.ts';
import { createNoteStore } from './notes.store.ts';

/** The handlers the notes feature contributes to `CqrsPlugin`. */
export function notesHandlers(): Required<
  Pick<CqrsPluginOptions, 'commandHandlers' | 'queryHandlers'>
> {
  const store = createNoteStore();
  return { commandHandlers: [addNoteHandler(store)], queryHandlers: [readNotesHandler(store)] };
}

/** Sends an `add-note` command through the command bus. */
export async function addNote(services: IServiceRegistry, text: string): Promise<void> {
  const command: AddNote = { type: ADD_NOTE, data: { text } };
  await services.get<ICommandBus>(CAPABILITIES.COMMAND_BUS).execute<void>(command);
}

/** Asks the query bus, which is independent of the command bus, for every note. */
export async function readNotes(services: IServiceRegistry): Promise<readonly string[]> {
  const query: ReadNotes = { type: READ_NOTES, data: {} };
  return await services.get<IQueryBus>(CAPABILITIES.QUERY_BUS).execute<readonly string[]>(query);
}

src/modules/notes/notes.commands.ts

import { CAPABILITIES } from '@setu-ts/common';
import type { CqrsCommand, ICommandHandler, IRuntimeServices } from '@setu-ts/common';
import type { CommandHandlerRegistration } from '@setu-ts/cqrs-plugin';
import type { NoteStore } from './notes.store.ts';

export const ADD_NOTE = 'add-note';

/** Records a note, stamped with the time it was added. */
export interface AddNote extends CqrsCommand<{ readonly text: string }> {
  readonly type: typeof ADD_NOTE;
}

/**
 * Registers the `add-note` handler as a factory.
 *
 * A factory runs once every plugin has registered, so it can resolve a
 * capability — here the runtime clock — that the command itself cannot carry.
 */
export function addNoteHandler(store: NoteStore): CommandHandlerRegistration {
  return {
    type: ADD_NOTE,
    handler: (services): ICommandHandler<AddNote, void> => {
      const runtime = services.get<IRuntimeServices>(CAPABILITIES.RUNTIME);
      return {
        handle: (command) => store.add(`${command.data.text} @ ${runtime.now()}`),
      };
    },
  };
}

src/modules/notes/notes.queries.ts

import type { CqrsQuery } from '@setu-ts/common';
import type { QueryHandlerRegistration } from '@setu-ts/cqrs-plugin';
import type { NoteStore } from './notes.store.ts';

export const READ_NOTES = 'read-notes';

/** Reads every recorded note. */
export interface ReadNotes extends CqrsQuery<Record<string, never>> {
  readonly type: typeof READ_NOTES;
}

/** Registers the `read-notes` handler as a plain instance, which needs no capability. */
export function readNotesHandler(store: NoteStore): QueryHandlerRegistration {
  return { type: READ_NOTES, handler: { handle: (): readonly string[] => store.all() } };
}

src/modules/notes/notes.store.ts

/** The notes the command side writes and the query side reads. */
export interface NoteStore {
  add(note: string): void;
  all(): readonly string[];
}

/** Creates an in-memory note store owned by one application. */
export function createNoteStore(): NoteStore {
  const notes: string[] = [];
  return {
    add: (note) => {
      notes.push(note);
    },
    all: () => [...notes],
  };
}

smoke.ts

import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import type { IKernelApplication } from '@setu-ts/kernel';
import { addNote, readNotes } from './src/modules/notes/index.ts';
import { createApp } from './setu.config.ts';

describe('cqrs example', () => {
  let app: IKernelApplication;

  before(async () => {
    app = createApp();
    await app.start();
  });

  after(async () => {
    await app.stop();
  });

  it('reads through the query bus what the command bus wrote', async () => {
    await addNote(app.services, 'separate buses');

    const notes = await readNotes(app.services);

    assert.equal(notes.length, 1);
    // The factory-built handler stamps each note with the runtime clock it resolved.
    assert.match(notes[0] ?? '', /^separate buses @ \d+$/);
  });
});