1import * as GCP from "alchemy/GCP";
2import * as Effect from "effect/Effect";
3import { HttpServerRequest } from "effect/http/HttpServerRequest";
4import * as HttpServerResponse from "effect/http/HttpServerResponse";
5import Summarize from "./Summarize.ts";
6
7/**
8 * An admin endpoint for running the nightly batch on demand — after a
9 * backfill, say, instead of waiting for the schedule.
10 *
11 * - `POST /run` — start a {@link Summarize} execution and return its
12 * operation name. Cloud Run Jobs are asynchronous: this answers as soon
13 * as the execution is created, not when it finishes.
14 *
15 * The service is public so the example can be driven with `curl`. In
16 * production, drop `invokerIamDisabled` and call it with an identity
17 * token instead.
18 */
19export default class Admin extends GCP.Function<Admin>()(
20 "Admin",
21 {
22 main: import.meta.url,
23 invokerIamDisabled: true,
24 },
25 Effect.gen(function* () {
26 // Grants this service's runtime account permission to run the job.
27 const runSummarize = yield* GCP.Run.RunJob(Summarize);
28
29 return {
30 fetch: Effect.gen(function* () {
31 const request = yield* HttpServerRequest;
32 const url = new URL(request.originalUrl);
33
34 if (request.method === "GET" && url.pathname === "/") {
35 return HttpServerResponse.text("ok");
36 }
37
38 if (request.method === "POST" && url.pathname === "/run") {
39 const operation = yield* runSummarize().pipe(Effect.orDie);
40 return yield* HttpServerResponse.json(
41 { operation: operation.name ?? null },
42 { status: 202 },
43 );
44 }
45
46 return yield* HttpServerResponse.json(
47 { error: "not found" },
48 { status: 404 },
49 );
50 }),
51 };
52 }).pipe(Effect.provide(GCP.Run.RunJobHttp)),
53) {}