1import * as CloudFront from "@/AWS/CloudFront";
2import * as Lambda from "@/AWS/Lambda";
3import * as S3 from "@/AWS/S3";
4import type * as cloudfront from "@distilled.cloud/aws/cloudfront";
5import * as Duration from "effect/Duration";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Schedule from "effect/Schedule";
9import { HttpServerRequest } from "effect/http/HttpServerRequest";
10import * as HttpServerResponse from "effect/http/HttpServerResponse";
11import path from "pathe";
12
13const main = path.resolve(import.meta.dirname, "handler.ts");
14
15// Distribution grants can still be propagating after the function URL is ready.
16const authorizationPolicy = {
17 while: (
18 error:
19 | cloudfront.CreateInvalidationError
20 | cloudfront.GetInvalidationError
21 | cloudfront.ListInvalidationsError,
22 ) => error._tag === "AccessDenied",
23 schedule: Schedule.spaced("3 seconds"),
24 times: 8,
25};
26
27export class CloudFrontTestFunction extends Lambda.Function<Lambda.Function>()(
28 "CloudFrontTestFunction",
29) {}
30
31export default CloudFrontTestFunction.make(
32 {
33 main,
34 functionUrl: true,
35 timeout: Duration.seconds(30),
36 },
37 Effect.gen(function* () {
38 const bucket = yield* S3.Bucket("InvalidationOriginBucket", {
39 forceDestroy: true,
40 });
41 const distribution = yield* CloudFront.Distribution("TestDistribution", {
42 // Deterministic marker so the test can find this distribution
43 // out-of-band via listDistributions.
44 comment: "alchemy-cf-bindings-fixture",
45 origins: [
46 {
47 id: "origin",
48 domainName: bucket.bucketRegionalDomainName,
49 s3Origin: true,
50 },
51 ],
52 defaultCacheBehavior: {
53 targetOriginId: "origin",
54 viewerProtocolPolicy: "redirect-to-https",
55 allowedMethods: ["GET", "HEAD"],
56 cachedMethods: ["GET", "HEAD"],
57 minTtl: 0,
58 forwardedValues: {
59 QueryString: false,
60 Cookies: { Forward: "none" },
61 },
62 },
63 });
64
65 const createInvalidation =
66 yield* CloudFront.CreateInvalidation(distribution);
67 const getInvalidation = yield* CloudFront.GetInvalidation(distribution);
68 const listInvalidations = yield* CloudFront.ListInvalidations(distribution);
69
70 return {
71 fetch: Effect.gen(function* () {
72 const request = yield* HttpServerRequest;
73 const url = new URL(request.originalUrl);
74 const pathname = url.pathname;
75
76 if (request.method === "GET" && pathname === "/health") {
77 return yield* HttpServerResponse.json({ ok: true });
78 }
79
80 if (request.method === "POST" && pathname === "/invalidate") {
81 const body = (yield* request.json) as unknown as {
82 callerReference: string;
83 paths: string[];
84 };
85 const response = yield* createInvalidation({
86 InvalidationBatch: {
87 CallerReference: body.callerReference,
88 Paths: {
89 Quantity: body.paths.length,
90 Items: body.paths,
91 },
92 },
93 }).pipe(Effect.retry(authorizationPolicy));
94 return yield* HttpServerResponse.json({
95 invalidationId: response.Invalidation?.Id,
96 status: response.Invalidation?.Status,
97 });
98 }
99
100 if (request.method === "GET" && pathname === "/invalidation") {
101 const id = url.searchParams.get("id")!;
102 const response = yield* getInvalidation({ Id: id }).pipe(
103 Effect.retry(authorizationPolicy),
104 );
105 return yield* HttpServerResponse.json({
106 invalidationId: response.Invalidation?.Id,
107 status: response.Invalidation?.Status,
108 });
109 }
110
111 if (request.method === "GET" && pathname === "/invalidations") {
112 const response = yield* listInvalidations({}).pipe(
113 Effect.retry(authorizationPolicy),
114 );
115 return yield* HttpServerResponse.json({
116 invalidationIds: (response.InvalidationList?.Items ?? []).map(
117 (item) => item.Id,
118 ),
119 });
120 }
121
122 return yield* HttpServerResponse.json(
123 { error: "Not found" },
124 { status: 404 },
125 );
126 }).pipe(Effect.orDie),
127 };
128 }).pipe(
129 Effect.provide(
130 Layer.mergeAll(
131 CloudFront.CreateInvalidationHttp,
132 CloudFront.GetInvalidationHttp,
133 CloudFront.ListInvalidationsHttp,
134 ),
135 ),
136 ),
137);