@arts-n-crafts/ts 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/index.cjs +659 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +420 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.js +625 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/core/utils/createCommand.ts
|
|
4
|
+
function createCommand(type, aggregateId, payload, metadata = {}) {
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
id: randomUUID(),
|
|
7
|
+
type,
|
|
8
|
+
aggregateId: String(aggregateId),
|
|
9
|
+
payload,
|
|
10
|
+
kind: "command",
|
|
11
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12
|
+
metadata
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function createQuery(type, payload, metadata = {}) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
id: randomUUID(),
|
|
18
|
+
type,
|
|
19
|
+
payload,
|
|
20
|
+
kind: "query",
|
|
21
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22
|
+
metadata
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/core/utils/isCommand.ts
|
|
27
|
+
function isCommand(candidate) {
|
|
28
|
+
if (candidate === null)
|
|
29
|
+
return false;
|
|
30
|
+
if (typeof candidate !== "object")
|
|
31
|
+
return false;
|
|
32
|
+
if (!("type" in candidate))
|
|
33
|
+
return false;
|
|
34
|
+
return "kind" in candidate && candidate.kind === "command";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/core/utils/isQuery.ts
|
|
38
|
+
function isQuery(candidate) {
|
|
39
|
+
if (candidate === null)
|
|
40
|
+
return false;
|
|
41
|
+
if (typeof candidate !== "object")
|
|
42
|
+
return false;
|
|
43
|
+
if (!("type" in candidate))
|
|
44
|
+
return false;
|
|
45
|
+
return "kind" in candidate && candidate.kind === "query";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/domain/Specification/Specification.ts
|
|
49
|
+
var Specification = class {
|
|
50
|
+
and(other) {
|
|
51
|
+
return new AndSpecification(this, other);
|
|
52
|
+
}
|
|
53
|
+
or(other) {
|
|
54
|
+
return new OrSpecification(this, other);
|
|
55
|
+
}
|
|
56
|
+
not() {
|
|
57
|
+
return new NotSpecification(this);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var AndSpecification = class extends Specification {
|
|
61
|
+
constructor(left, right) {
|
|
62
|
+
super();
|
|
63
|
+
this.left = left;
|
|
64
|
+
this.right = right;
|
|
65
|
+
}
|
|
66
|
+
isSatisfiedBy(entity) {
|
|
67
|
+
return this.left.isSatisfiedBy(entity) && this.right.isSatisfiedBy(entity);
|
|
68
|
+
}
|
|
69
|
+
toQuery() {
|
|
70
|
+
return {
|
|
71
|
+
type: "and",
|
|
72
|
+
nodes: [this.left.toQuery(), this.right.toQuery()]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var OrSpecification = class extends Specification {
|
|
77
|
+
constructor(left, right) {
|
|
78
|
+
super();
|
|
79
|
+
this.left = left;
|
|
80
|
+
this.right = right;
|
|
81
|
+
}
|
|
82
|
+
isSatisfiedBy(entity) {
|
|
83
|
+
return this.left.isSatisfiedBy(entity) || this.right.isSatisfiedBy(entity);
|
|
84
|
+
}
|
|
85
|
+
toQuery() {
|
|
86
|
+
return {
|
|
87
|
+
type: "or",
|
|
88
|
+
nodes: [this.left.toQuery(), this.right.toQuery()]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var NotSpecification = class extends Specification {
|
|
93
|
+
constructor(spec) {
|
|
94
|
+
super();
|
|
95
|
+
this.spec = spec;
|
|
96
|
+
}
|
|
97
|
+
isSatisfiedBy(entity) {
|
|
98
|
+
return !this.spec.isSatisfiedBy(entity);
|
|
99
|
+
}
|
|
100
|
+
toQuery() {
|
|
101
|
+
return {
|
|
102
|
+
type: "not",
|
|
103
|
+
node: this.spec.toQuery()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/domain/Specification/utils/createQueryNode.ts
|
|
109
|
+
function createQueryNode(type, field, value) {
|
|
110
|
+
switch (type) {
|
|
111
|
+
case "eq":
|
|
112
|
+
case "gt":
|
|
113
|
+
case "lt":
|
|
114
|
+
return { type, field, value };
|
|
115
|
+
case "and":
|
|
116
|
+
case "or":
|
|
117
|
+
return { type, nodes: value };
|
|
118
|
+
case "not":
|
|
119
|
+
return { type, node: value };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/domain/Specification/implementations/FieldEquals.specification.ts
|
|
124
|
+
var FieldEquals = class extends Specification {
|
|
125
|
+
constructor(field, value) {
|
|
126
|
+
super();
|
|
127
|
+
this.field = field;
|
|
128
|
+
this.value = value;
|
|
129
|
+
}
|
|
130
|
+
isSatisfiedBy(entity) {
|
|
131
|
+
return entity[this.field] === this.value;
|
|
132
|
+
}
|
|
133
|
+
toQuery() {
|
|
134
|
+
return createQueryNode("eq", this.field, this.value);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/utils/fail/fail.ts
|
|
139
|
+
function fail(anExpression) {
|
|
140
|
+
return () => {
|
|
141
|
+
throw anExpression;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/utils/invariant/invariant.ts
|
|
146
|
+
function invariant(condition, onInvalid) {
|
|
147
|
+
if (!condition)
|
|
148
|
+
onInvalid();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/domain/Specification/implementations/FieldGreaterThan.specification.ts
|
|
152
|
+
var FieldGreaterThan = class extends Specification {
|
|
153
|
+
constructor(field, value) {
|
|
154
|
+
super();
|
|
155
|
+
this.field = field;
|
|
156
|
+
this.value = value;
|
|
157
|
+
}
|
|
158
|
+
isNumber(value) {
|
|
159
|
+
return typeof value === "number";
|
|
160
|
+
}
|
|
161
|
+
isSatisfiedBy(entity) {
|
|
162
|
+
const field = entity[this.field];
|
|
163
|
+
invariant(
|
|
164
|
+
this.isNumber(field),
|
|
165
|
+
fail(
|
|
166
|
+
new TypeError(`Field ${String(this.field)} is not a number`)
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
return field > this.value;
|
|
170
|
+
}
|
|
171
|
+
toQuery() {
|
|
172
|
+
return createQueryNode("gt", this.field, this.value);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function createDomainEvent(type, aggregateId, payload, metadata = {}) {
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
id: randomUUID(),
|
|
178
|
+
type,
|
|
179
|
+
aggregateId,
|
|
180
|
+
payload,
|
|
181
|
+
source: "internal",
|
|
182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
183
|
+
metadata
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/domain/utils/isEvent.ts
|
|
188
|
+
function isEvent(event) {
|
|
189
|
+
if (typeof event !== "object")
|
|
190
|
+
return false;
|
|
191
|
+
if (event === null)
|
|
192
|
+
return false;
|
|
193
|
+
if (!("type" in event))
|
|
194
|
+
return false;
|
|
195
|
+
return "source" in event;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/domain/utils/isDomainEvent.ts
|
|
199
|
+
function isDomainEvent(event) {
|
|
200
|
+
return isEvent(event) && "aggregateId" in event && event.source === "internal";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/infrastructure/CommandBus/implementations/SimpleCommandBus.ts
|
|
204
|
+
var SimpleCommandBus = class {
|
|
205
|
+
handlers = /* @__PURE__ */ new Map();
|
|
206
|
+
register(aTypeOfCommand, anHandler) {
|
|
207
|
+
if (this.handlers.has(aTypeOfCommand)) {
|
|
208
|
+
throw new Error(`Handler already registered for command type: ${aTypeOfCommand}`);
|
|
209
|
+
}
|
|
210
|
+
this.handlers.set(aTypeOfCommand, anHandler);
|
|
211
|
+
}
|
|
212
|
+
async execute(aCommand) {
|
|
213
|
+
const handler = this.handlers.get(aCommand.type);
|
|
214
|
+
if (!handler) {
|
|
215
|
+
throw new Error(`No handler found for command type: ${aCommand.type}`);
|
|
216
|
+
}
|
|
217
|
+
return handler.execute(aCommand);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/infrastructure/Database/Database.ts
|
|
222
|
+
var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
223
|
+
Operation2["CREATE"] = "CREATE";
|
|
224
|
+
Operation2["PUT"] = "PUT";
|
|
225
|
+
Operation2["PATCH"] = "PATCH";
|
|
226
|
+
Operation2["DELETE"] = "DELETE";
|
|
227
|
+
return Operation2;
|
|
228
|
+
})(Operation || {});
|
|
229
|
+
|
|
230
|
+
// src/infrastructure/Database/implementations/SimpleDatabase.exceptions.ts
|
|
231
|
+
var RecordNotFoundException = class extends Error {
|
|
232
|
+
constructor(id) {
|
|
233
|
+
super(`Record not found for id: ${id}`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
var DuplicateRecordException = class extends Error {
|
|
237
|
+
constructor(id) {
|
|
238
|
+
super(`Duplicate record found for id: ${id}`);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var DatabaseOfflineException = class extends Error {
|
|
242
|
+
constructor() {
|
|
243
|
+
super(`Database is offline`);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/infrastructure/Database/implementations/SimpleDatabase.ts
|
|
248
|
+
var SimpleDatabase = class {
|
|
249
|
+
datasource = /* @__PURE__ */ new Map();
|
|
250
|
+
simulateOffline = false;
|
|
251
|
+
async query(tableName, specification) {
|
|
252
|
+
if (this.simulateOffline)
|
|
253
|
+
throw new DatabaseOfflineException();
|
|
254
|
+
const tableRecords = this.datasource.get(tableName) || [];
|
|
255
|
+
return tableRecords.filter((record) => specification.isSatisfiedBy(record));
|
|
256
|
+
}
|
|
257
|
+
async execute(tableName, statement) {
|
|
258
|
+
if (this.simulateOffline)
|
|
259
|
+
throw new DatabaseOfflineException();
|
|
260
|
+
if (!this.datasource.has(tableName))
|
|
261
|
+
this.datasource.set(tableName, []);
|
|
262
|
+
const table = this.datasource.get(tableName);
|
|
263
|
+
switch (statement.operation) {
|
|
264
|
+
case "CREATE" /* CREATE */: {
|
|
265
|
+
const isDuplicate = table.some((item) => item.id === statement.payload.id);
|
|
266
|
+
if (isDuplicate)
|
|
267
|
+
throw new DuplicateRecordException(statement.payload.id);
|
|
268
|
+
table.push(statement.payload);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "PUT" /* PUT */: {
|
|
272
|
+
const index = table.findIndex((item) => item.id === statement.payload.id);
|
|
273
|
+
if (index === -1)
|
|
274
|
+
throw new RecordNotFoundException(statement.payload.id);
|
|
275
|
+
table[index] = statement.payload;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "PATCH" /* PATCH */: {
|
|
279
|
+
const index = table.findIndex((item) => item.id === statement.payload.id);
|
|
280
|
+
if (index === -1)
|
|
281
|
+
throw new RecordNotFoundException(statement.payload.id);
|
|
282
|
+
table[index] = { ...table[index], ...statement.payload };
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "DELETE" /* DELETE */: {
|
|
286
|
+
const index = table.findIndex((item) => item.id === statement.payload.id);
|
|
287
|
+
if (index === -1)
|
|
288
|
+
throw new RecordNotFoundException(statement.payload.id);
|
|
289
|
+
table.splice(index, 1);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
goOffline() {
|
|
295
|
+
this.simulateOffline = true;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/infrastructure/EventBus/implementations/SimpleEventBus.ts
|
|
300
|
+
var SimpleEventBus = class {
|
|
301
|
+
handlers = /* @__PURE__ */ new Map();
|
|
302
|
+
subscribe(stream, aHandler) {
|
|
303
|
+
const handlersForType = this.handlers.get(stream) ?? [];
|
|
304
|
+
handlersForType.push(aHandler);
|
|
305
|
+
this.handlers.set(stream, handlersForType);
|
|
306
|
+
}
|
|
307
|
+
async consume(stream, anEvent) {
|
|
308
|
+
if (!this.handlers.has(stream))
|
|
309
|
+
return;
|
|
310
|
+
const handlers = this.handlers.get(stream);
|
|
311
|
+
await Promise.all(handlers.map(async (handler) => {
|
|
312
|
+
return handler.handle(anEvent);
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
async publish(stream, anEvent) {
|
|
316
|
+
await this.consume(stream, anEvent);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function createIntegrationEvent(type, payload, metadata) {
|
|
320
|
+
return Object.freeze({
|
|
321
|
+
id: randomUUID(),
|
|
322
|
+
type,
|
|
323
|
+
payload,
|
|
324
|
+
source: "external",
|
|
325
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
326
|
+
metadata: {
|
|
327
|
+
...metadata
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/infrastructure/EventBus/utils/isIntegrationEvent.ts
|
|
333
|
+
function isIntegrationEvent(event) {
|
|
334
|
+
return isEvent(event) && event.source === "external";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/utils/isEqual/isEqual.ts
|
|
338
|
+
function isEqual(a, b) {
|
|
339
|
+
if (a === b) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
const bothAreObjects = a && b && typeof a === "object" && typeof b === "object";
|
|
343
|
+
return Boolean(
|
|
344
|
+
bothAreObjects && Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([k, v]) => isEqual(v, b[k]))
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/utils/parseAsError/parseAsError.ts
|
|
349
|
+
function parseAsError(value) {
|
|
350
|
+
if (value instanceof Error)
|
|
351
|
+
return value;
|
|
352
|
+
if (typeof value === "string")
|
|
353
|
+
return new Error(value);
|
|
354
|
+
try {
|
|
355
|
+
const json = JSON.stringify(value);
|
|
356
|
+
return new Error(json ?? String(value));
|
|
357
|
+
} catch {
|
|
358
|
+
return new Error("Unknown error");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/utils/streamKey/makeStreamKey.ts
|
|
363
|
+
function makeStreamKey(streamName, aggregateId) {
|
|
364
|
+
return `${streamName}#${aggregateId}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/infrastructure/EventStore/utils/createStoredEvent.ts
|
|
368
|
+
function createStoredEvent(streamName, version, event) {
|
|
369
|
+
return Object.freeze({
|
|
370
|
+
id: event.id,
|
|
371
|
+
streamKey: makeStreamKey(streamName, event.aggregateId),
|
|
372
|
+
version,
|
|
373
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
374
|
+
event
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/infrastructure/EventStore/implementations/SimpleEventStore.exceptions.ts
|
|
379
|
+
var MultipleAggregatesException = class extends Error {
|
|
380
|
+
constructor() {
|
|
381
|
+
super("EventStore append does not support multiple aggregates to be stored");
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/infrastructure/EventStore/implementations/SimpleEventStore.ts
|
|
386
|
+
var SimpleEventStore = class {
|
|
387
|
+
constructor(database, outbox) {
|
|
388
|
+
this.database = database;
|
|
389
|
+
this.outbox = outbox;
|
|
390
|
+
}
|
|
391
|
+
tableName = "event_store";
|
|
392
|
+
async load(streamName, aggregateId) {
|
|
393
|
+
const streamKey = makeStreamKey(streamName, aggregateId);
|
|
394
|
+
const specification = new FieldEquals("streamKey", streamKey);
|
|
395
|
+
const storedEvents = await this.database.query(this.tableName, specification);
|
|
396
|
+
return storedEvents.map((storedEvent) => storedEvent.event);
|
|
397
|
+
}
|
|
398
|
+
async append(streamName, events) {
|
|
399
|
+
const uniqueAggregateIds = new Set(events.map((event2) => event2.aggregateId));
|
|
400
|
+
if (uniqueAggregateIds.size > 1)
|
|
401
|
+
throw new MultipleAggregatesException();
|
|
402
|
+
const event = events[0];
|
|
403
|
+
const currentStream = await this.load(streamName, event.aggregateId);
|
|
404
|
+
const eventsToStore = events.map((event2) => createStoredEvent(streamName, currentStream.length + 1, event2));
|
|
405
|
+
await Promise.all(
|
|
406
|
+
eventsToStore.map(
|
|
407
|
+
async (payload) => this.database.execute(this.tableName, { operation: "CREATE" /* CREATE */, payload })
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
await Promise.all(events.map(async (event2) => this.outbox?.enqueue(event2)));
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/infrastructure/Outbox/implementations/GenericOutboxWorker.ts
|
|
415
|
+
var GenericOutboxWorker = class {
|
|
416
|
+
constructor(outbox, eventBus, stream) {
|
|
417
|
+
this.outbox = outbox;
|
|
418
|
+
this.eventBus = eventBus;
|
|
419
|
+
this.stream = stream;
|
|
420
|
+
}
|
|
421
|
+
async runOnce() {
|
|
422
|
+
const pending = await this.outbox.getPending();
|
|
423
|
+
await Promise.all(pending.map(async (entry) => {
|
|
424
|
+
try {
|
|
425
|
+
await this.eventBus.publish(this.stream, entry.event);
|
|
426
|
+
await this.outbox.markAsPublished(entry.id);
|
|
427
|
+
} catch {
|
|
428
|
+
await this.outbox.markAsFailed(entry.id);
|
|
429
|
+
}
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
async tick() {
|
|
433
|
+
await this.runOnce();
|
|
434
|
+
}
|
|
435
|
+
start(intervalMs) {
|
|
436
|
+
setInterval(() => {
|
|
437
|
+
void this.tick().catch(console.error);
|
|
438
|
+
}, intervalMs);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/infrastructure/Outbox/implementations/InMemoryOutbox.ts
|
|
443
|
+
var InMemoryOutbox = class {
|
|
444
|
+
entries = [];
|
|
445
|
+
idCounter = 0;
|
|
446
|
+
async enqueue(event) {
|
|
447
|
+
this.entries.push({
|
|
448
|
+
id: (this.idCounter++).toString(),
|
|
449
|
+
event,
|
|
450
|
+
published: false,
|
|
451
|
+
retryCount: 0
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async getPending(limit = 100) {
|
|
455
|
+
return this.entries.filter((e) => !e.published).slice(0, limit);
|
|
456
|
+
}
|
|
457
|
+
async markAsPublished(id) {
|
|
458
|
+
const entry = this.entries.find((e) => e.id === id);
|
|
459
|
+
if (entry) {
|
|
460
|
+
entry.published = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async markAsFailed(id) {
|
|
464
|
+
const entry = this.entries.find((e) => e.id === id);
|
|
465
|
+
if (entry) {
|
|
466
|
+
entry.retryCount += 1;
|
|
467
|
+
entry.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
function createOutboxEntry(event) {
|
|
472
|
+
return Object.freeze({
|
|
473
|
+
id: randomUUID(),
|
|
474
|
+
event,
|
|
475
|
+
published: false,
|
|
476
|
+
retryCount: 0,
|
|
477
|
+
lastAttemptAt: void 0
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/infrastructure/QueryBus/implementations/SimpleQueryBus.ts
|
|
482
|
+
var SimpleQueryBus = class {
|
|
483
|
+
handlers = /* @__PURE__ */ new Map();
|
|
484
|
+
register(aTypeOfQuery, anHandler) {
|
|
485
|
+
if (this.handlers.has(aTypeOfQuery)) {
|
|
486
|
+
throw new Error(`Handler already registered for query type: ${aTypeOfQuery}`);
|
|
487
|
+
}
|
|
488
|
+
this.handlers.set(aTypeOfQuery, anHandler);
|
|
489
|
+
}
|
|
490
|
+
async execute(aQuery) {
|
|
491
|
+
const handler = this.handlers.get(aQuery.type);
|
|
492
|
+
if (!handler) {
|
|
493
|
+
throw new Error(`No handler found for query type: ${aQuery.type}`);
|
|
494
|
+
}
|
|
495
|
+
return handler.execute(aQuery);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// src/infrastructure/Repository/implementations/SimpleRepository.ts
|
|
500
|
+
var SimpleRepository = class {
|
|
501
|
+
constructor(eventStore, streamName, evolveFn, initialState) {
|
|
502
|
+
this.eventStore = eventStore;
|
|
503
|
+
this.streamName = streamName;
|
|
504
|
+
this.evolveFn = evolveFn;
|
|
505
|
+
this.initialState = initialState;
|
|
506
|
+
}
|
|
507
|
+
async load(aggregateId) {
|
|
508
|
+
const pastEvents = await this.eventStore.load(this.streamName, aggregateId);
|
|
509
|
+
return pastEvents.reduce(this.evolveFn, this.initialState(aggregateId));
|
|
510
|
+
}
|
|
511
|
+
async store(events) {
|
|
512
|
+
await Promise.all(
|
|
513
|
+
events.map(
|
|
514
|
+
async (event) => this.eventStore.append(
|
|
515
|
+
this.streamName,
|
|
516
|
+
[event]
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// src/infrastructure/ScenarioTest/ScenarioTest.ts
|
|
524
|
+
var ScenarioTest = class {
|
|
525
|
+
constructor(streamName, eventBus, eventStore, commandBus, queryBus, repository, outboxWorker) {
|
|
526
|
+
this.streamName = streamName;
|
|
527
|
+
this.eventBus = eventBus;
|
|
528
|
+
this.eventStore = eventStore;
|
|
529
|
+
this.commandBus = commandBus;
|
|
530
|
+
this.queryBus = queryBus;
|
|
531
|
+
this.repository = repository;
|
|
532
|
+
this.outboxWorker = outboxWorker;
|
|
533
|
+
}
|
|
534
|
+
givenInput = [];
|
|
535
|
+
whenInput;
|
|
536
|
+
given(...events) {
|
|
537
|
+
this.givenInput = events;
|
|
538
|
+
return {
|
|
539
|
+
when: this.when.bind(this),
|
|
540
|
+
then: this.then.bind(this)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
when(action) {
|
|
544
|
+
this.whenInput = action;
|
|
545
|
+
return {
|
|
546
|
+
then: this.then.bind(this)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
async then(thenInput) {
|
|
550
|
+
const domainEvents = this.givenInput.filter(isDomainEvent);
|
|
551
|
+
const integrationEvents = this.givenInput.filter(isIntegrationEvent);
|
|
552
|
+
await Promise.all([
|
|
553
|
+
this.repository.store(domainEvents),
|
|
554
|
+
integrationEvents.map(async (event) => this.eventBus.consume(this.streamName, event))
|
|
555
|
+
]);
|
|
556
|
+
if (!this.whenInput) {
|
|
557
|
+
throw new Error("In the ScenarioTest, the when-step cannot be empty");
|
|
558
|
+
}
|
|
559
|
+
if (isCommand(this.whenInput)) {
|
|
560
|
+
invariant(isDomainEvent(thenInput), fail(new TypeError('When "command" expects a domain event in the then-step')));
|
|
561
|
+
await this.handleCommand(this.whenInput, thenInput);
|
|
562
|
+
}
|
|
563
|
+
if (isQuery(this.whenInput)) {
|
|
564
|
+
invariant(Array.isArray(thenInput), fail(new TypeError('When "query" expects an array of expected results in the then-step')));
|
|
565
|
+
await this.handleQuery(this.whenInput, thenInput);
|
|
566
|
+
}
|
|
567
|
+
if (isDomainEvent(this.whenInput) || isIntegrationEvent(this.whenInput)) {
|
|
568
|
+
invariant(isDomainEvent(thenInput), fail(new TypeError('When "domain event" or "integration event" expects a domain event in the then-step')));
|
|
569
|
+
await this.handleEvent(this.whenInput, thenInput);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async handleCommand(command, outcome) {
|
|
573
|
+
await this.commandBus.execute(command);
|
|
574
|
+
const actualEvents = await this.eventStore.load(this.streamName, outcome.aggregateId);
|
|
575
|
+
const foundEvent = actualEvents.findLast(
|
|
576
|
+
(event) => isDomainEvent(event) && event.aggregateId === outcome.aggregateId && event.type === outcome.type
|
|
577
|
+
);
|
|
578
|
+
invariant(!!foundEvent, fail(new Error("ScenarioTest: event/command was not found")));
|
|
579
|
+
invariant(
|
|
580
|
+
outcome.type === foundEvent.type,
|
|
581
|
+
fail(new Error("ScenarioTest: event/command type was not equal"))
|
|
582
|
+
);
|
|
583
|
+
invariant(
|
|
584
|
+
outcome.aggregateId === foundEvent.aggregateId,
|
|
585
|
+
fail(new Error("ScenarioTest: event/command aggregate id was not equal"))
|
|
586
|
+
);
|
|
587
|
+
invariant(
|
|
588
|
+
isEqual(outcome.payload, foundEvent.payload),
|
|
589
|
+
fail(new Error("ScenarioTest: event/command payload was not equal"))
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
async handleQuery(query, expected) {
|
|
593
|
+
await this.outboxWorker.tick();
|
|
594
|
+
const actual = await this.queryBus.execute(query);
|
|
595
|
+
invariant(
|
|
596
|
+
isEqual(actual, expected),
|
|
597
|
+
fail(new Error("ScenarioTest: a different query result was returned"))
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
async handleEvent(event, outcome) {
|
|
601
|
+
await this.eventBus.publish(this.streamName, event);
|
|
602
|
+
const actualEvents = await this.eventStore.load(this.streamName, outcome.aggregateId);
|
|
603
|
+
const foundEvent = actualEvents.findLast(
|
|
604
|
+
(event2) => isEvent(event2) && event2.aggregateId === outcome.aggregateId && event2.type === outcome.type
|
|
605
|
+
);
|
|
606
|
+
invariant(!!foundEvent, fail(new Error("ScenarioTest: event was not found")));
|
|
607
|
+
invariant(isEvent(foundEvent), fail(new Error("ScenarioTest: event is not of type event")));
|
|
608
|
+
invariant(
|
|
609
|
+
outcome.type === foundEvent.type,
|
|
610
|
+
fail(new Error("ScenarioTest: event type was not equal"))
|
|
611
|
+
);
|
|
612
|
+
invariant(
|
|
613
|
+
outcome.aggregateId === foundEvent.aggregateId,
|
|
614
|
+
fail(new Error("ScenarioTest: event aggregate id was not equal"))
|
|
615
|
+
);
|
|
616
|
+
invariant(
|
|
617
|
+
isEqual(outcome.payload, foundEvent.payload),
|
|
618
|
+
fail(new Error("ScenarioTest: event payload was not equal"))
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
export { AndSpecification, FieldEquals, FieldGreaterThan, GenericOutboxWorker, InMemoryOutbox, NotSpecification, Operation, OrSpecification, ScenarioTest, SimpleCommandBus, SimpleDatabase, SimpleEventBus, SimpleEventStore, SimpleQueryBus, SimpleRepository, Specification, createCommand, createDomainEvent, createIntegrationEvent, createOutboxEntry, createQuery, createQueryNode, createStoredEvent, fail, invariant, isCommand, isDomainEvent, isEqual, isEvent, isIntegrationEvent, isQuery, makeStreamKey, parseAsError };
|
|
624
|
+
//# sourceMappingURL=index.js.map
|
|
625
|
+
//# sourceMappingURL=index.js.map
|