Skip to content

Commit 9cb5450

Browse files
authored
New event store and projection store abstractions (#20)
2 parents 4fac478 + f177531 commit 9cb5450

18 files changed

Lines changed: 1226 additions & 169 deletions

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"main": "dist/index.js",
55
"scripts": {
66
"build": "tsc",
7+
"watch": "tsc --watch",
78
"start": "tsx src/index.ts",
89
"test": "tsx tests/unit/main.ts",
910
"format": "prettier --write .",
@@ -14,6 +15,7 @@
1415
"class-validator": "^0.14.2",
1516
"express": "^4.18.2",
1617
"fluture": "^14.0.0",
18+
"luxon": "^3.7.2",
1719
"minio": "^8.0.5",
1820
"mongodb": "^5.4.0",
1921
"nodemailer": "^7.0.5",
@@ -29,6 +31,7 @@
2931
},
3032
"devDependencies": {
3133
"@types/express": "^4.17.21",
34+
"@types/luxon": "^3.7.1",
3235
"@types/node": "^24.3.0",
3336
"@types/nodemailer": "^7.0.1",
3437
"@types/pg": "^8.6.6",

src/app/commandHandler.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export { handleCommand };
2+
3+
import { Response } from '@/lib/router';
4+
import { EventStore } from '@/lib/eventSourcing/eventStore';
5+
import { Decoder, decode } from '@/lib/json/decoder';
6+
import * as express from 'express';
7+
import * as router from '@/lib/router';
8+
import { Future } from '@/lib/Future';
9+
import { Result, Failure } from '@/lib/Result';
10+
11+
type Projections = {};
12+
type Services = {};
13+
14+
type CommandController<Command> = {
15+
decoder: Decoder<Command>;
16+
handler: (v: {
17+
command: Command;
18+
store: EventStore;
19+
projections: Projections;
20+
services: Services;
21+
}) => Future<Response, Response>;
22+
};
23+
24+
function handleCommand<Command>(
25+
withEventStore: <T>(f: (store: EventStore) => T) => T,
26+
services: Services,
27+
projections: Projections,
28+
{ decoder, handler }: CommandController<Command>,
29+
): express.Handler {
30+
return router.route((req) =>
31+
decodeCommand(decoder, req).chain((command) =>
32+
withEventStore((store) =>
33+
handler({
34+
command,
35+
store,
36+
projections,
37+
services,
38+
}),
39+
),
40+
),
41+
);
42+
}
43+
44+
function decodeCommand<C>(
45+
decoder: Decoder<C>,
46+
req: express.Request,
47+
): Future<Response, C> {
48+
const decoded: Result<string, C> = decode(decoder, req.body);
49+
if (decoded instanceof Failure) {
50+
return Future.reject(
51+
router.json({
52+
status: 400,
53+
content: { message: `Unable to decode command: ${decoded.error}` },
54+
}),
55+
);
56+
}
57+
58+
return Future.resolve(decoded.value);
59+
}

src/app/event.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
Aggregate,
3+
Id,
4+
CreationEvent,
5+
TransformationEvent,
6+
} from '@/lib/eventSourcing/event';
7+
8+
import * as s from '@/lib/json/schema';
9+
10+
class User implements Aggregate<User> {
11+
constructor(
12+
readonly aggregateId: Id<User>,
13+
readonly aggregateVersion: number,
14+
readonly name: string,
15+
) {}
16+
}
17+
18+
export class CreateUser implements CreationEvent<CreateUser, User> {
19+
static type: 'CreateUserr' = 'CreateUserr';
20+
static schemaArgs = s.object({
21+
type: s.stringLiteral(CreateUser.type),
22+
aggregateId: Id.schema(),
23+
name: s.string,
24+
});
25+
static schema = CreateUser.schemaArgs.dimap(
26+
(v) => new CreateUser(v),
27+
(v) => v.values,
28+
);
29+
30+
schema = CreateUser.schema;
31+
constructor(readonly values: s.Infer<typeof CreateUser.schemaArgs>) {}
32+
createAggregate() {
33+
return new User(new Id('wat'), 0, this.values.name);
34+
}
35+
}
36+
37+
export class AddName implements TransformationEvent<AddName, User> {
38+
static type: 'AddName' = 'AddName';
39+
constructor(readonly values: s.Infer<typeof AddName.schemaArgs>) {}
40+
41+
static schemaArgs = s.object({
42+
type: s.stringLiteral(AddName.type),
43+
aggregateId: Id.schema(),
44+
name: s.string,
45+
});
46+
47+
static schema = AddName.schemaArgs.dimap(
48+
(v) => new AddName(v),
49+
(v) => v.values,
50+
);
51+
readonly schema = AddName.schema;
52+
53+
transformAggregate(agg: User): User {
54+
const u = new User(
55+
agg.aggregateId,
56+
agg.aggregateVersion + 1,
57+
this.values.name,
58+
);
59+
return u;
60+
}
61+
}
62+
63+
export class RemoveName implements TransformationEvent<RemoveName, User> {
64+
static type: 'RemoveName' = 'RemoveName';
65+
constructor(readonly values: s.Infer<typeof RemoveName.schemaArgs>) {}
66+
67+
static schemaArgs = s.object({
68+
type: s.stringLiteral(RemoveName.type),
69+
aggregateId: Id.schema(),
70+
name: s.string,
71+
});
72+
73+
static schema = RemoveName.schemaArgs.dimap(
74+
(v) => new RemoveName(v),
75+
(v) => v.values,
76+
);
77+
readonly schema = RemoveName.schema;
78+
79+
transformAggregate(agg: User): User {
80+
const u = new User(
81+
agg.aggregateId,
82+
agg.aggregateVersion + 1,
83+
this.values.name,
84+
);
85+
return u;
86+
}
87+
}

0 commit comments

Comments
 (0)