Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
336f255
Add first command handler stub
lazamar Sep 26, 2025
5ca9ebe
Implement toSchema
lazamar Oct 1, 2025
7593ca6
Solution proposal
lazamar Oct 1, 2025
e68bad0
Update eventStore API
lazamar Oct 1, 2025
9eefc6c
Remove EventClass from projectionHandler
lazamar Oct 1, 2025
9dad34e
Make hydrator take object of entries
lazamar Oct 1, 2025
76d957f
Make hydrator be able to decode events
lazamar Oct 1, 2025
fa83683
Loosen restrictions on Hydrator constructor
lazamar Oct 1, 2025
40a86e6
Remove Self type parameter from Event
lazamar Oct 1, 2025
732bd5a
Introduce first event with Hydrator
lazamar Oct 1, 2025
6e4b928
Rename Hydrator to Schemas
lazamar Oct 1, 2025
0126d68
Factor out schemas
lazamar Oct 1, 2025
7a50ea3
Incorporate id generation into the Id type
lazamar Oct 6, 2025
055bff5
Implement submitApplication command
lazamar Oct 6, 2025
4832e0d
Ignore tags
lazamar Oct 6, 2025
e3835f4
Add standard responses
lazamar Oct 7, 2025
68d5237
Abort transactions on failure at eventStore
lazamar Oct 7, 2025
f948edc
Add Ambar API handling module
lazamar Oct 8, 2025
0e04819
Introduce AmbarResponse type
lazamar Oct 8, 2025
f110c5c
Implement evaluateApplication reaction
lazamar Oct 8, 2025
73e33a7
Use concrete event store instance in reactions
lazamar Oct 8, 2025
1a32829
Abstract creation of commands and reactions
lazamar Oct 8, 2025
ae0ca87
Remove projections, commands and reactions from aggregate subfolders
lazamar Oct 8, 2025
3d557cd
Add to schema
lazamar Oct 8, 2025
5355c24
Start implementation of MongoProjectionStore
lazamar Oct 8, 2025
51d5a67
Use withProjectionStore in projectionHandler
lazamar Oct 9, 2025
a00c55e
Implement membersByCuisine projection
lazamar Oct 13, 2025
361e499
Implement membersByCuisine query
lazamar Oct 13, 2025
d73ccda
Use correct HTTP verbs
lazamar Oct 13, 2025
adcdf53
Remove legacy implementations
lazamar Oct 13, 2025
10f5529
Move ambar to /lib
lazamar Oct 13, 2025
f736854
Rename handlers
lazamar Oct 13, 2025
a76fd26
Better names for application parts
lazamar Oct 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
node_modules/
dist/
*.log
tags
59 changes: 0 additions & 59 deletions src/app/commandHandler.ts

This file was deleted.

87 changes: 0 additions & 87 deletions src/app/event.ts

This file was deleted.

28 changes: 15 additions & 13 deletions src/app/postgresEventStore.ts → src/app/eventStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { initialize, PostgresEventStore };
export { initialize, PostgresEventStore, type WithEventStore };

import { Json } from '@/lib/json/types';
import {
Expand All @@ -7,26 +7,28 @@ import {
Aggregate,
CreationEvent,
TransformationEvent,
EventClass,
EventInfo,
} from '@/lib/eventSourcing/event';
import {
EventStore,
Hydrator,
Schemas,
Constructor,
EventData,
schema_EventData,
} from '@/lib/eventSourcing/eventStore';
import { PostgresTransaction } from '@/lib/postgres';
import { log } from '@/common/util/Logger';
import { IdGenerator } from '@/common/util/IdGenerator';
import { encode } from '@/lib/json/schema';
import { POSIX } from '@/lib/time';
import { Future } from '@/lib/Future';

type WithEventStore = <E, T>(
onError: (e: Error) => E,
f: (s: EventStore) => Future<E, T>,
) => Future<E, T>;

class PostgresEventStore implements EventStore {
constructor(
private transaction: PostgresTransaction,
private readonly hydrator: Hydrator,
private readonly schemas: Schemas,
private readonly eventStoreTable: string,
) {}

Expand All @@ -35,22 +37,22 @@ class PostgresEventStore implements EventStore {
aggregateId: Id<T>,
): Promise<{ aggregate: T; lastEvent: EventInfo }> {
const events = await this.findAll(aggregateId);
const { lastEvent, aggregate } = this.hydrator
const { lastEvent, aggregate } = this.schemas
.hydrate(cls, events)
.unwrap((e) => e);

return { aggregate, lastEvent };
}

async save<E extends Event<T>, T extends Aggregate<T>>(args: {
async emit<T extends Aggregate<T>>(args: {
aggregate: Constructor<T>;
event: CreationEvent<E, T> | TransformationEvent<E, T>;
event: CreationEvent<T> | TransformationEvent<T>;
event_id?: Id<Event<T>>;
correlation_id?: Id<Event<T>>;
causation_id?: Id<Event<T>>;
}): Promise<void> {
const event = args.event;
const event_id = args.event_id || new Id(IdGenerator.generateRandomId());
const event_id = args.event_id || Id.random();
let info: EventInfo;
switch (true) {
case event instanceof CreationEvent: {
Expand Down Expand Up @@ -121,14 +123,14 @@ class PostgresEventStore implements EventStore {
}
}

private async insert<E extends EventClass<E, any>>(edata: EventData<E>) {
private async insert<E extends Event<any>>(edata: EventData<E>) {
const sql = `
INSERT INTO ${this.eventStoreTable} (
event_id, aggregate_id, causation_id, correlation_id,
aggregate_version, json_payload, json_metadata, recorded_on, event_name
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`;

const serialized = encode(schema_EventData(edata.event.schema), edata);
const serialized = this.schemas.encode(edata);

const values = [
// @ts-ignore
Expand Down
21 changes: 21 additions & 0 deletions src/app/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Schemas for all application events
*/
export { schemas };

import { Schemas, CSchema, TSchema } from '@/lib/eventSourcing/eventStore';
import { ApplicationSubmitted } from '@/domain/cookingClub/membership2/events/membership/applicationSubmitted';
import { ApplicationEvaluated } from '@/domain/cookingClub/membership2/events/membership/applicationEvaluated';

const schemas = new Schemas([
new CSchema(
ApplicationSubmitted.aggregate,
ApplicationSubmitted.schema,
ApplicationSubmitted.type,
),
new TSchema(
ApplicationEvaluated.aggregate,
ApplicationEvaluated.schema,
ApplicationEvaluated.type,
),
]);
71 changes: 71 additions & 0 deletions src/app/handleCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export { handleCommand, type CommandController, type CommandHandler };

import { Response } from '@/lib/router';
import { EventStore } from '@/lib/eventSourcing/eventStore';
import { Decoder, decode } from '@/lib/json/decoder';
import * as express from 'express';
import * as router from '@/lib/router';
import { Future } from '@/lib/Future';
import { Result, Failure } from '@/lib/Result';
import { Repositories, Projections, allProjections } from '@/app/projections';
import { Services } from '@/app/services';
import { WithProjectionStore } from '@/app/projectionStore';
import { WithEventStore } from '@/app/eventStore';

type CommandHandler<Command> = (v: {
command: Command;
store: EventStore;
projections: Projections;
services: Services;
}) => Future<Response, Response>;

type CommandController<Command> = {
decoder: Decoder<Command>;
handler: CommandHandler<Command>;
};

const onStoreError = (_: Error): Response =>
router.json({
status: 500,
content: { message: 'Internal Server Error' },
});

function handleCommand<Command>(
withEventStore: WithEventStore,
withProjectionStore: WithProjectionStore,
services: Services,
repositories: Repositories,
{ decoder, handler }: CommandController<Command>,
): express.Handler {
return router.route((req) =>
decodeCommand(decoder, req).chain((command) =>
withProjectionStore(onStoreError, (projectionStore) =>
withEventStore(onStoreError, (store) =>
handler({
command,
store,
projections: allProjections(repositories, projectionStore),
services,
}),
),
),
),
);
}

function decodeCommand<C>(
decoder: Decoder<C>,
req: express.Request,
): Future<Response, C> {
const decoded: Result<string, C> = decode(decoder, req.body);
if (decoded instanceof Failure) {
return Future.reject(
router.json({
status: 400,
content: { message: `Unable to decode command: ${decoded.error}` },
}),
);
}

return Future.resolve(decoded.value);
}
Loading