Skip to content

Commit 1122688

Browse files
committed
Extract stored event ids
1 parent a489623 commit 1122688

File tree

9 files changed

+140
-54
lines changed

9 files changed

+140
-54
lines changed

src/dependencies.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import {Logger} from 'pino';
2-
import {Failure, Email, DomainEvent, ResourceVersion} from './types';
2+
import {
3+
Failure,
4+
Email,
5+
DomainEvent,
6+
ResourceVersion,
7+
StoredDomainEvent,
8+
StoredEventOfType,
9+
} from './types';
310
import * as TE from 'fp-ts/TaskEither';
411
import * as O from 'fp-ts/Option';
512
import {FailureWithStatus} from './types/failure-with-status';
613
import {StatusCodes} from 'http-status-codes';
714

815
import {Resource} from './types/resource';
9-
import {EventName, EventOfType} from './types/domain-event';
16+
import {EventName} from './types/domain-event';
1017
import {SharedReadModel} from './read-models/shared-state';
1118
import {
1219
SheetDataTable,
@@ -25,15 +32,15 @@ export type Dependencies = {
2532
>;
2633
getAllEvents: () => TE.TaskEither<
2734
FailureWithStatus,
28-
ReadonlyArray<DomainEvent>
35+
ReadonlyArray<StoredDomainEvent>
2936
>;
3037
getAllEventsByType: <T extends EventName>(
3138
eventType: T
32-
) => TE.TaskEither<FailureWithStatus, ReadonlyArray<EventOfType<T>>>;
39+
) => TE.TaskEither<FailureWithStatus, ReadonlyArray<StoredEventOfType<T>>>;
3340
getResourceEvents: (resource: Resource) => TE.TaskEither<
3441
FailureWithStatus,
3542
{
36-
events: ReadonlyArray<DomainEvent>;
43+
events: ReadonlyArray<StoredDomainEvent>;
3744
version: ResourceVersion;
3845
}
3946
>;

src/init-dependencies/event-store/events-from-rows.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as E from 'fp-ts/Either';
33
import * as tt from 'io-ts-types';
44
import {EventsTable} from './events-table';
55
import * as t from 'io-ts';
6-
import {DomainEvent} from '../../types';
6+
import {StoredDomainEvent} from '../../types';
77
import {internalCodecFailure} from '../../types/failure-with-status';
88

99
const reshapeRowToEvent = (row: EventsTable['rows'][number]) =>
@@ -12,6 +12,7 @@ const reshapeRowToEvent = (row: EventsTable['rows'][number]) =>
1212
tt.JsonFromString.decode,
1313
E.chain(tt.JsonRecord.decode),
1414
E.map(payload => ({
15+
event_id: row.id,
1516
type: row.event_type,
1617
...payload,
1718
}))
@@ -21,6 +22,6 @@ export const eventsFromRows = (rows: EventsTable['rows']) =>
2122
pipe(
2223
rows,
2324
E.traverseArray(reshapeRowToEvent),
24-
E.chain(t.readonlyArray(DomainEvent).decode),
25+
E.chain(t.readonlyArray(StoredDomainEvent).decode),
2526
E.mapLeft(internalCodecFailure('Failed to get events from DB'))
2627
);

src/init-dependencies/event-store/get-all-events.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {EventsTable} from './events-table';
1010
import {eventsFromRows} from './events-from-rows';
1111
import {Client} from '@libsql/client';
1212
import {StatusCodes} from 'http-status-codes';
13-
import {DomainEvent} from '../../types';
14-
import {EventName, EventOfType} from '../../types/domain-event';
13+
import {StoredDomainEvent, StoredEventOfType} from '../../types';
14+
import {EventName} from '../../types/domain-event';
1515
import {dbExecute} from '../../util';
1616

1717
export const getAllEvents =
@@ -69,8 +69,11 @@ export const getAllEventsByType =
6969
// This assumes that the DB has only returned events of the correct type.
7070
// This assumption avoids the need to do extra validation.
7171
// TODO - Pass codec to validate straight to eventsFromRows and get best of both.
72-
TE.map<ReadonlyArray<DomainEvent>, ReadonlyArray<EventOfType<T>>>(
73-
es => es as ReadonlyArray<EventOfType<T>>
72+
TE.map<
73+
ReadonlyArray<StoredDomainEvent>,
74+
ReadonlyArray<StoredEventOfType<T>>
75+
>(
76+
es => es as ReadonlyArray<StoredEventOfType<T>>
7477
)
7578
);
7679

@@ -106,7 +109,10 @@ export const getAllEventsByTypes =
106109
// This assumption avoids the need to do extra validation.
107110
// TODO - Pass codec to validate straight to eventsFromRows and get best of both.
108111
TE.map<
109-
ReadonlyArray<DomainEvent>,
110-
ReadonlyArray<EventOfType<T> | EventOfType<R>>
111-
>(es => es as ReadonlyArray<EventOfType<T> | EventOfType<R>>)
112+
ReadonlyArray<StoredDomainEvent>,
113+
ReadonlyArray<StoredEventOfType<T> | StoredEventOfType<R>>
114+
>(
115+
es =>
116+
es as ReadonlyArray<StoredEventOfType<T> | StoredEventOfType<R>>
117+
)
112118
);

src/sync-worker/dependencies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from './google/sheet-data-table';
1010
import {ReadonlyRecord} from 'fp-ts/lib/ReadonlyRecord';
1111
import {UUID} from 'io-ts-types';
12-
import {DomainEvent, Email, Failure, ResourceVersion} from '../types';
12+
import {Email, Failure, ResourceVersion, StoredDomainEvent} from '../types';
1313
import {SharedReadModel} from '../read-models/shared-state';
1414
import {Resource} from '../types/resource';
1515
import {Dependencies} from '../dependencies';
@@ -54,7 +54,7 @@ export interface SyncWorkerDependencies {
5454
getResourceEvents: (resource: Resource) => TE.TaskEither<
5555
FailureWithStatus,
5656
{
57-
events: ReadonlyArray<DomainEvent>;
57+
events: ReadonlyArray<StoredDomainEvent>;
5858
version: ResourceVersion;
5959
}
6060
>;

src/types/domain-event.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,22 @@ export const DomainEvent = t.union([
283283
TrainingStatNotificationSent.codec,
284284
]);
285285

286+
export const StoredDomainEvent = t.intersection([
287+
DomainEvent,
288+
t.strict({
289+
event_id: tt.UUID,
290+
}),
291+
]);
292+
286293
export type DomainEvent = t.TypeOf<typeof DomainEvent>;
294+
export type StoredDomainEvent = t.TypeOf<typeof StoredDomainEvent>;
287295

288296
export type EventName = DomainEvent['type'];
289297

290298
export type EventOfType<T extends EventName> = DomainEvent & {type: T};
299+
export type StoredEventOfType<T extends EventName> = StoredDomainEvent & {
300+
type: T;
301+
};
291302

292303
export const isEventOfType =
293304
<T extends EventName>(name: T) =>

src/types/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ export type {GravatarHash} from './gravatar-hash';
1010
export {isoGravatarHash} from './gravatar-hash';
1111
export {
1212
DomainEvent,
13+
StoredDomainEvent,
1314
isEventOfType,
1415
constructEvent,
1516
filterByName,
1617
} from './domain-event';
17-
export type {SubsetOfDomainEvent} from './domain-event';
18+
export type {
19+
StoredEventOfType,
20+
SubsetOfDomainEvent,
21+
} from './domain-event';
1822
export type {ResourceVersion} from './resource-version';

tests/init-dependencies/event-store/end-to-end.test.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import {faker} from '@faker-js/faker';
33
import * as libsqlClient from '@libsql/client';
44
import * as E from 'fp-ts/Either';
55
import * as T from 'fp-ts/Task';
6-
import {DomainEvent, EmailAddress, constructEvent} from '../../../src/types';
6+
import {
7+
DomainEvent,
8+
EmailAddress,
9+
StoredDomainEvent,
10+
constructEvent,
11+
} from '../../../src/types';
712
import {getAllEvents} from '../../../src/init-dependencies/event-store/get-all-events';
813
import {pipe} from 'fp-ts/lib/function';
914
import {
@@ -29,12 +34,20 @@ const arbitraryMemberNumberLinkedToEmailEvent = () =>
2934
const testLogger = createLogger({level: 'silent'});
3035
const dummyRefreshReadModel = () => T.of(undefined);
3136

37+
const expectStoredEvent = (
38+
actualEvent: StoredDomainEvent,
39+
expectedEvent: DomainEvent
40+
) => {
41+
expect(actualEvent).toMatchObject(expectedEvent);
42+
expect(actualEvent.event_id).toEqual(expect.any(String));
43+
};
44+
3245
describe('event-store end-to-end', () => {
3346
describe('setup event store', () => {
3447
const resource = {id: 'singleton', type: 'MemberNumberEmailPairings'};
3548
const event = arbitraryMemberNumberLinkedToEmailEvent();
3649
let dbClient: libsqlClient.Client;
37-
let getTestEvents: () => Promise<ReadonlyArray<DomainEvent>>;
50+
let getTestEvents: () => Promise<ReadonlyArray<StoredDomainEvent>>;
3851
let resourceEvents: RightOfTaskEither<
3952
ReturnType<Dependencies['getResourceEvents']>
4053
>;
@@ -82,7 +95,9 @@ describe('event-store end-to-end', () => {
8295
});
8396

8497
it('persists the event', async () => {
85-
expect(await getTestEvents()).toStrictEqual([event]);
98+
const events = await getTestEvents();
99+
expect(events).toHaveLength(1);
100+
expectStoredEvent(events[0], event);
86101
});
87102

88103
it('uses the initial version number', () => {
@@ -104,7 +119,10 @@ describe('event-store end-to-end', () => {
104119
});
105120

106121
it('persists the event', async () => {
107-
expect(await getTestEvents()).toStrictEqual([event, event2]);
122+
const events = await getTestEvents();
123+
expect(events).toHaveLength(2);
124+
expectStoredEvent(events[0], event);
125+
expectStoredEvent(events[1], event2);
108126
});
109127

110128
it('increments the version', () => {
@@ -135,7 +153,8 @@ describe('event-store end-to-end', () => {
135153
it('does not persist the event', async () => {
136154
const events = await getTestEvents();
137155
expect(events).toHaveLength(2);
138-
expect(events).toStrictEqual([initialEvent, competingEvent]);
156+
expectStoredEvent(events[0], initialEvent);
157+
expectStoredEvent(events[1], competingEvent);
139158
});
140159

141160
it.failing('returns on left', () => {
@@ -173,7 +192,8 @@ describe('event-store end-to-end', () => {
173192

174193
it('has independant events', async () => {
175194
expect(await getTestEvents()).toHaveLength(3);
176-
expect(resourceEvents.events).toStrictEqual([event]);
195+
expect(resourceEvents.events).toHaveLength(1);
196+
expectStoredEvent(resourceEvents.events[0], event);
177197
});
178198
});
179199
});

tests/init-dependencies/event-store/get-all-events.test.ts

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {randomUUID} from 'crypto';
66
import {
77
DomainEvent,
88
EmailAddress,
9+
StoredDomainEvent,
910
constructEvent,
1011
} from '../../../src/types';
1112
import {commitEvent} from '../../../src/init-dependencies/event-store/commit-event';
@@ -49,11 +50,24 @@ describe('get all events', () => {
4950
const testLogger = createLogger({level: 'silent'});
5051
const dummyRefreshReadModel = () => T.of(undefined);
5152

53+
const expectStoredEvent = (
54+
actualEvent: StoredDomainEvent,
55+
expectedEvent: DomainEvent
56+
) => {
57+
expect(actualEvent).toMatchObject(expectedEvent);
58+
expect(actualEvent.event_id).toEqual(expect.any(String));
59+
};
60+
5261
let dbClient: libsqlClient.Client;
5362
let persistEvent: (event: DomainEvent) => Promise<void>;
54-
let initalisedGetAllEvents: () => Promise<ReadonlyArray<DomainEvent>>;
55-
let initalisedGetAllEventsByType: (type: EventName) => Promise<ReadonlyArray<DomainEvent>>;
56-
let initalisedGetAllEventsByTypes: (type1: EventName, type2: EventName) => Promise<ReadonlyArray<DomainEvent>>;
63+
let initalisedGetAllEvents: () => Promise<ReadonlyArray<StoredDomainEvent>>;
64+
let initalisedGetAllEventsByType: (
65+
type: EventName
66+
) => Promise<ReadonlyArray<StoredDomainEvent>>;
67+
let initalisedGetAllEventsByTypes: (
68+
type1: EventName,
69+
type2: EventName
70+
) => Promise<ReadonlyArray<StoredDomainEvent>>;
5771

5872
beforeEach(async () => {
5973
dbClient = libsqlClient.createClient({url: ':memory:'});
@@ -87,10 +101,10 @@ describe('get all events', () => {
87101
await persistEvent(memberNumberLinkedToEmail);
88102
await persistEvent(equipmentTrainingQuizResult);
89103
await persistEvent(equipmentTrainingSheetRegistered);
90-
expect(await initalisedGetAllEvents()).toStrictEqual([
91-
memberNumberLinkedToEmail,
92-
equipmentTrainingSheetRegistered,
93-
]);
104+
const events = await initalisedGetAllEvents();
105+
expect(events).toHaveLength(2);
106+
expectStoredEvent(events[0], memberNumberLinkedToEmail);
107+
expectStoredEvent(events[1], equipmentTrainingSheetRegistered);
94108
});
95109
});
96110

@@ -102,10 +116,12 @@ describe('get all events', () => {
102116
await persistEvent(firstMatchingEvent);
103117
await persistEvent(nonMatchingEvent);
104118
await persistEvent(secondMatchingEvent);
105-
expect(await initalisedGetAllEventsByType('MemberNumberLinkedToEmail')).toStrictEqual([
106-
firstMatchingEvent,
107-
secondMatchingEvent,
108-
]);
119+
const events = await initalisedGetAllEventsByType(
120+
'MemberNumberLinkedToEmail'
121+
);
122+
expect(events).toHaveLength(2);
123+
expectStoredEvent(events[0], firstMatchingEvent);
124+
expectStoredEvent(events[1], secondMatchingEvent);
109125
});
110126

111127
it('returns EquipmentTrainingQuizResult events when explicitly requested', async () => {
@@ -114,7 +130,11 @@ describe('get all events', () => {
114130
const nonMatchingEvent = arbitraryMemberNumberLinkedToEmailEvent();
115131
await persistEvent(equipmentTrainingQuizResult);
116132
await persistEvent(nonMatchingEvent);
117-
expect(await initalisedGetAllEventsByType('EquipmentTrainingQuizResult')).toStrictEqual([equipmentTrainingQuizResult]);
133+
const events = await initalisedGetAllEventsByType(
134+
'EquipmentTrainingQuizResult'
135+
);
136+
expect(events).toHaveLength(1);
137+
expectStoredEvent(events[0], equipmentTrainingQuizResult);
118138
});
119139
});
120140

@@ -128,15 +148,13 @@ describe('get all events', () => {
128148
await persistEvent(nonMatchingEvent);
129149
await persistEvent(secondMatchingEvent);
130150

131-
expect(
132-
await initalisedGetAllEventsByTypes(
133-
'MemberNumberLinkedToEmail',
134-
'EquipmentTrainingSheetRegistered'
135-
)
136-
).toStrictEqual([
137-
firstMatchingEvent,
138-
secondMatchingEvent,
139-
]);
151+
const events = await initalisedGetAllEventsByTypes(
152+
'MemberNumberLinkedToEmail',
153+
'EquipmentTrainingSheetRegistered'
154+
);
155+
expect(events).toHaveLength(2);
156+
expectStoredEvent(events[0], firstMatchingEvent);
157+
expectStoredEvent(events[1], secondMatchingEvent);
140158
});
141159

142160
it('returns EquipmentTrainingQuizResult events when one of the requested types matches', async () => {
@@ -149,15 +167,13 @@ describe('get all events', () => {
149167
await persistEvent(matchingEvent);
150168
await persistEvent(nonMatchingEvent);
151169

152-
expect(
153-
await initalisedGetAllEventsByTypes(
154-
'EquipmentTrainingQuizResult',
155-
'EquipmentTrainingSheetRegistered'
156-
)
157-
).toStrictEqual([
158-
equipmentTrainingQuizResult,
159-
matchingEvent,
160-
]);
170+
const events = await initalisedGetAllEventsByTypes(
171+
'EquipmentTrainingQuizResult',
172+
'EquipmentTrainingSheetRegistered'
173+
);
174+
expect(events).toHaveLength(2);
175+
expectStoredEvent(events[0], equipmentTrainingQuizResult);
176+
expectStoredEvent(events[1], matchingEvent);
161177
});
162178
});
163179
});

0 commit comments

Comments
 (0)