1import * as BackupSearch from "@/AWS/BackupSearch";
2import * as Lambda from "@/AWS/Lambda";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import path from "pathe";
8
9const main = path.resolve(import.meta.dirname, "handler.ts");
10
11// Deterministic name so the crash-safe sweeper in Bindings.test.ts can find
12// (and stop) any job a crashed prior run left RUNNING.
13export const FIXTURE_SEARCH_JOB_NAME = "alchemy-test-backupsearch-bindings";
14
15export class BackupSearchTestFunction extends Lambda.Function<Lambda.Function>()(
16 "BackupSearchTestFunction",
17) {}
18
19export default BackupSearchTestFunction.make(
20 {
21 main,
22 functionUrl: true,
23 },
24 Effect.gen(function* () {
25 // The fixture search job scans S3 recovery points for a prefix that
26 // matches nothing — the job's lifecycle (and both list operations) are
27 // identical whether or not the account has indexed recovery points.
28 const search = yield* BackupSearch.SearchJob("BindingsSearch", {
29 name: FIXTURE_SEARCH_JOB_NAME,
30 searchScope: { backupResourceTypes: ["S3"] },
31 itemFilters: {
32 s3ItemFilters: [
33 {
34 objectKeys: [
35 { value: "alchemy-bindings-", operator: "BEGINS_WITH" },
36 ],
37 },
38 ],
39 },
40 tags: { fixture: "backup-search-bindings" },
41 });
42
43 // --- search-job-scoped bindings ---
44 const listSearchJobResults =
45 yield* BackupSearch.ListSearchJobResults(search);
46 const listSearchJobBackups =
47 yield* BackupSearch.ListSearchJobBackups(search);
48 const getSearchJob = yield* BackupSearch.GetSearchJob(search);
49
50 const bound = { listSearchJobResults, listSearchJobBackups, getSearchJob };
51
52 return {
53 fetch: Effect.gen(function* () {
54 const request = yield* HttpServerRequest;
55 const url = new URL(request.originalUrl);
56 const pathname = url.pathname;
57
58 if (request.method === "GET" && pathname === "/bindings") {
59 return yield* HttpServerResponse.json({
60 bound: Object.keys(bound),
61 });
62 }
63
64 if (request.method === "GET" && pathname === "/results") {
65 const page = yield* listSearchJobResults({ MaxResults: 25 });
66 return yield* HttpServerResponse.json({
67 count: page.Results.length,
68 nextToken: page.NextToken ?? null,
69 });
70 }
71
72 if (request.method === "GET" && pathname === "/job") {
73 const job = yield* getSearchJob();
74 return yield* HttpServerResponse.json({
75 status: job.Status ?? null,
76 name: job.Name ?? null,
77 });
78 }
79
80 if (request.method === "GET" && pathname === "/backups") {
81 const page = yield* listSearchJobBackups({ MaxResults: 25 });
82 return yield* HttpServerResponse.json({
83 count: page.Results.length,
84 statuses: page.Results.map((backup) => backup.Status ?? null),
85 });
86 }
87
88 return yield* HttpServerResponse.json(
89 { error: "Not found", method: request.method, pathname },
90 { status: 404 },
91 );
92 }).pipe(Effect.orDie),
93 };
94 }).pipe(
95 Effect.provide(
96 Layer.mergeAll(
97 BackupSearch.ListSearchJobResultsHttp,
98 BackupSearch.ListSearchJobBackupsHttp,
99 BackupSearch.GetSearchJobHttp,
100 ),
101 ),
102 ),
103);