Skip to content

Commit f6f43ef

Browse files
committed
Wiring up a way to view exclusion log
1 parent 23100b1 commit f6f43ef

File tree

13 files changed

+187
-6
lines changed

13 files changed

+187
-6
lines changed

src/dependencies.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import * as TE from 'fp-ts/TaskEither';
44
import * as O from 'fp-ts/Option';
55
import {FailureWithStatus} from './types/failure-with-status';
66
import {StatusCodes} from 'http-status-codes';
7-
87
import {Resource} from './types/resource';
98
import {EventName, EventOfType} from './types/domain-event';
109
import {SharedReadModel} from './read-models/shared-state';
1110
import {
1211
SheetDataTable,
1312
TroubleTicketDataTable,
1413
} from './sync-worker/google/sheet-data-table';
14+
import { ExcludedEvent } from './init-dependencies/event-store/excluded-event';
1515

1616
export type Dependencies = {
1717
commitEvent: (
@@ -32,6 +32,10 @@ export type Dependencies = {
3232
FailureWithStatus,
3333
ReadonlyArray<DomainEvent>
3434
>;
35+
getAllExclusionEvents: () => TE.TaskEither<
36+
FailureWithStatus,
37+
ReadonlyArray<ExcludedEvent>
38+
>;
3539
getAllEventsByType: <T extends EventName>(
3640
eventType: T
3741
) => TE.TaskEither<FailureWithStatus, ReadonlyArray<EventOfType<T>>>;

src/init-dependencies/event-store/ensure-events-table-exists.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ export const ensureEventTableExists = (eventDB: Client) =>
3333
CREATE TABLE IF NOT EXISTS events_exclusions (
3434
id TEXT PRIMARY KEY,
3535
event_id TEXT NOT NULL,
36-
reverted_by_number number NOT NULL,
37-
revert_reason TEXT NOT NULL
36+
reverted_by_member_number INTEGER NOT NULL,
37+
revert_reason TEXT NOT NULL,
38+
reverted_at_timestamp_epoch_ms INTEGER NOT NULL
3839
);
3940
CREATE INDEX IF NOT EXISTS events_exclusions_event_id_idx
4041
ON events_exclusions (event_id);

src/init-dependencies/event-store/events-table.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ export const EventExclusionsTable = t.strict({
1919
t.strict({
2020
id: t.string,
2121
event_id: t.string,
22-
reverted_by_number: t.Int,
22+
reverted_by_member_number: t.Int,
2323
revert_reason: t.string,
24+
reverted_at_timestamp_epoch_ms: t.Int,
25+
payload: t.string,
26+
event_type: t.string,
2427
})
2528
),
2629
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Int } from "io-ts";
2+
import { DomainEvent } from "../../types";
3+
4+
export type ExcludedEvent = {
5+
id: string,
6+
event_id: string,
7+
reverted_by_number: Int,
8+
revert_reason: string,
9+
revert_at: Date,
10+
payload: DomainEvent,
11+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {pipe} from 'fp-ts/lib/function';
2+
import * as E from 'fp-ts/Either';
3+
import * as tt from 'io-ts-types';
4+
import {EventExclusionsTable} from './events-table';
5+
import * as t from 'io-ts';
6+
import {DomainEvent} from '../../types';
7+
import {internalCodecFailure} from '../../types/failure-with-status';
8+
import { ExcludedEvent } from './excluded-event';
9+
10+
const reshapeRowToEvent = (row: EventExclusionsTable['rows'][number]) =>
11+
pipe(
12+
row.payload,
13+
tt.JsonFromString.decode,
14+
E.chain(tt.JsonRecord.decode),
15+
E.map(payload => ({
16+
type: row.event_type,
17+
...payload,
18+
}))
19+
);
20+
21+
export const exclusionEventsFromRows = (rows: EventExclusionsTable['rows']): ReadonlyArray<ExcludedEvent> =>
22+
pipe(
23+
rows,
24+
E.traverseArray(reshapeRowToEvent),
25+
E.chain(t.readonlyArray(DomainEvent).decode),
26+
E.mapLeft(internalCodecFailure('Failed to get events from DB'))
27+
);

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import {
66
} from '../../types/failure-with-status';
77
import * as TE from 'fp-ts/TaskEither';
88
import * as E from 'fp-ts/Either';
9-
import {EventsTable} from './events-table';
9+
import * as RA from 'fp-ts/ReadonlyArray';
10+
import {EventExclusionsTable, EventsTable} from './events-table';
1011
import {eventsFromRows} from './events-from-rows';
1112
import {Client} from '@libsql/client';
1213
import {StatusCodes} from 'http-status-codes';
1314
import {DomainEvent} from '../../types';
1415
import {EventName, EventOfType} from '../../types/domain-event';
1516
import {dbExecute} from '../../util';
17+
import { exclusionEventsFromRows } from './exclusion-events-from-rows';
1618

1719
export const getAllEvents =
1820
(dbClient: Client): Dependencies['getAllEvents'] =>
@@ -132,3 +134,32 @@ export const getAllEventsByTypes =
132134
ReadonlyArray<EventOfType<T> | EventOfType<R>>
133135
>(es => es as ReadonlyArray<EventOfType<T> | EventOfType<R>>)
134136
);
137+
138+
export const getAllExclusionEvents = (dbClient: Client): Dependencies['getAllExclusionEvents'] =>
139+
() =>
140+
pipe(
141+
TE.tryCatch(
142+
() =>
143+
dbExecute(
144+
dbClient,
145+
`
146+
SELECT events_exclusions.*, events.payload, events.event_type
147+
FROM events_exclusions
148+
INNER JOIN events ON events.id = events_exclusions.event_id
149+
`,
150+
{}
151+
),
152+
failureWithStatus(
153+
'Failed to query database',
154+
StatusCodes.INTERNAL_SERVER_ERROR
155+
)
156+
),
157+
TE.chainEitherK(
158+
flow(
159+
EventExclusionsTable.decode,
160+
E.mapLeft(internalCodecFailure('Failed to decode event exclusions DB table'))
161+
)
162+
),
163+
TE.map(table => table.rows),
164+
TE.map(exclusionEventsFromRows)
165+
);

src/init-dependencies/init-dependencies.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as O from 'fp-ts/Option';
66
import createLogger, {LoggerOptions} from 'pino';
77
import nodemailer from 'nodemailer';
88
import {commitEvent} from './event-store/commit-event';
9-
import {getAllEvents, getAllEventsByType} from './event-store/get-all-events';
9+
import {getAllEvents, getAllEventsByType, getAllExclusionEvents} from './event-store/get-all-events';
1010
import {getResourceEvents} from './event-store/get-resource-events';
1111
import {Client} from '@libsql/client';
1212

@@ -73,6 +73,7 @@ export const initDependencies = (
7373
commitEvent: commitEvent(eventDB, logger, sharedReadModel.asyncRefresh),
7474
excludeEvent: excludeEvent(eventDB),
7575
getAllEvents: getAllEvents(eventDB),
76+
getAllExclusionEvents: getAllExclusionEvents(eventDB),
7677
getAllEventsByType: getAllEventsByType(eventDB),
7778
getResourceEvents: getResourceEvents(eventDB),
7879
sharedReadModel,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {pipe} from 'fp-ts/lib/function';
2+
import {User} from '../../types';
3+
import {Dependencies} from '../../dependencies';
4+
import * as TE from 'fp-ts/TaskEither';
5+
import {readModels} from '../../read-models';
6+
import {FailureWithStatus, failureWithStatus} from '../../types/failure-with-status';
7+
import {StatusCodes} from 'http-status-codes';
8+
import { ViewModel } from './view-model';
9+
10+
11+
export const constructViewModel =
12+
(deps: Dependencies) => (user: User) => (): TE.TaskEither<FailureWithStatus, ViewModel> =>
13+
pipe(
14+
deps.getAllEvents(),
15+
TE.filterOrElse(readModels.superUsers.is(user.memberNumber), () =>
16+
failureWithStatus(
17+
'You do not have the necessary permission to see this page.',
18+
StatusCodes.FORBIDDEN
19+
)()
20+
),
21+
TE.chain(
22+
(_events) => deps.getAllExclusionEvents()
23+
),
24+
TE.map(
25+
events => ({events})
26+
)
27+
);

src/queries/exclusion-log/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {pipe} from 'fp-ts/lib/function';
2+
import * as TE from 'fp-ts/TaskEither';
3+
import {render} from './render';
4+
import {constructViewModel} from './construct-view-model';
5+
import {Query, Params} from '../query';
6+
import {safe, toLoggedInContent} from '../../types/html';
7+
import {User} from '../../types';
8+
9+
export const exclusionLog: Query = deps => (user: User) =>
10+
pipe(
11+
constructViewModel(deps)(user)(),
12+
TE.map(render),
13+
TE.map(toLoggedInContent(safe('Event Log')))
14+
);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {pipe} from 'fp-ts/lib/function';
2+
import * as RA from 'fp-ts/ReadonlyArray';
3+
import {html, joinHtml, sanitizeString} from '../../types/html';
4+
import {ViewModel} from './view-model';
5+
import {DomainEvent} from '../../types';
6+
import {inspect} from 'node:util';
7+
import {displayDate} from '../../templates/display-date';
8+
import {DateTime} from 'luxon';
9+
import { renderMemberNumber } from '../../templates/member-number';
10+
11+
const renderPayload = (event: DomainEvent) =>
12+
// eslint-disable-next-line unused-imports/no-unused-vars
13+
pipe(event, ({type, actor, recordedAt, ...payload}) =>
14+
pipe(
15+
payload,
16+
Object.entries,
17+
RA.map(([key, value]) => `${key}: ${inspect(value)}`),
18+
RA.map(sanitizeString),
19+
joinHtml
20+
)
21+
);
22+
23+
const renderEntry = (event: ViewModel['events'][number]) => html`
24+
<li>
25+
<b>EXCLUDED by ${renderMemberNumber(event.reverted_by_number)} at ${displayDate(DateTime.fromJSDate(event.revert_at))}
26+
because '${sanitizeString(event.revert_reason)}'.</b>
27+
${renderPayload(event.payload)}
28+
</li>
29+
`;
30+
31+
const renderLog = (log: ViewModel['events']) =>
32+
pipe(
33+
log,
34+
RA.map(renderEntry),
35+
joinHtml,
36+
items => html`
37+
<ul>
38+
${items}
39+
</ul>
40+
`
41+
);
42+
43+
export const render = (viewModel: ViewModel) => html`
44+
<h1>Event log</h1>
45+
<p>There are ${viewModel.events.length} excluded events</p>
46+
${renderLog(viewModel.events)}
47+
`;

0 commit comments

Comments
 (0)