1import * as GCP from "alchemy/GCP";
2import * as Effect from "effect/Effect";
3import * as HttpServerResponse from "effect/http/HttpServerResponse";
4import { LOCATION, Shop } from "./resources.ts";
5
6/**
7 * Reacts to every new order by writing an `audit/{id}` document.
8 *
9 * The service is private (the default): only the Eventarc trigger, which
10 * signs each delivery with this service's own identity, can invoke it.
11 *
12 * Firestore events arrive as `application/protobuf`, so `event.data` is a
13 * `Uint8Array` of a `DocumentEventData` message. The CloudEvent attributes
14 * already name the document (`document: "orders/{id}"`), which is all an
15 * audit trail needs; a reaction that needs the fields either decodes the
16 * protobuf or reads the document back by that path.
17 */
18export default class Auditor extends GCP.Function<Auditor>()(
19 "Auditor",
20 {
21 main: import.meta.url,
22 },
23 Effect.gen(function* () {
24 const shop = yield* Shop;
25 // roles/datastore.user on the project, conditioned on this database.
26 const db = yield* GCP.Firestore.WriteDatabase(shop);
27
28 // Grants this service's account roles/eventarc.eventReceiver on the
29 // project and roles/run.invoker on the service, then creates the
30 // trigger in the database's location.
31 yield* GCP.Eventarc.consumeEvents(
32 "OrderCreated",
33 {
34 location: LOCATION,
35 eventFilters: [
36 {
37 attribute: "type",
38 value: "google.cloud.firestore.document.v1.created",
39 },
40 { attribute: "database", value: shop.databaseId },
41 {
42 attribute: "document",
43 value: "orders/{id}",
44 operator: "match-path-pattern",
45 },
46 ],
47 },
48 (event) =>
49 Effect.gen(function* () {
50 const document = event.attributes.document ?? "";
51 const id = document.split("/")[1];
52 if (id === undefined) return;
53 yield* Effect.log(`audit ${document} (${event.type} ${event.id})`);
54
55 // `create` makes redelivery harmless: Eventarc is at-least-once,
56 // and a second delivery of the same event finds the audit entry
57 // already there.
58 yield* db
59 .create(`audit/${id}`, {
60 order: document,
61 eventId: event.id,
62 eventType: event.type,
63 eventTime: event.time ?? null,
64 payloadBytes:
65 event.data instanceof Uint8Array ? event.data.length : null,
66 })
67 .pipe(
68 Effect.catchTag(
69 "GCP.Firestore.DocumentAlreadyExists",
70 () => Effect.void,
71 ),
72 // A failure answers 500, and Eventarc redelivers later.
73 Effect.orDie,
74 );
75 }),
76 );
77
78 return { fetch: Effect.succeed(HttpServerResponse.text("ok")) };
79 }).pipe(
80 Effect.provide(GCP.Firestore.WriteDatabaseHttp),
81 Effect.provide(GCP.Run.EventarcEventSource),
82 ),
83) {}