1import * as Lambda from "@/AWS/Lambda";
2import * as MediaTailor from "@/AWS/MediaTailor";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12// Channel-assembly names are runtime request parameters; these nonexistent
13// names drive the typed not-found paths (proving the IAM grants reached the
14// API — a missing grant would surface AccessDeniedException instead).
15export const NONEXISTENT_CHANNEL = "alchemy-nonexistent-mediatailor-channel";
16export const NONEXISTENT_PROGRAM = "alchemy-nonexistent-mediatailor-program";
17
18export class MediaTailorTestFunction extends Lambda.Function<Lambda.Function>()(
19 "MediaTailorTestFunction",
20) {}
21
22export default MediaTailorTestFunction.make(
23 {
24 main,
25 functionUrl: true,
26 // MediaTailor control-plane calls routinely take a few seconds; AWS's
27 // default 3s Lambda timeout is too tight under cold starts.
28 timeout: Duration.seconds(30),
29 },
30 Effect.gen(function* () {
31 const config = yield* MediaTailor.PlaybackConfiguration("BindingsConfig", {
32 adDecisionServerUrl: "https://ads.example.com/vast?ip=[client_ip]",
33 videoContentSourceUrl: "https://origin.example.com/live",
34 });
35
36 const createPrefetchSchedule =
37 yield* MediaTailor.CreatePrefetchSchedule(config);
38 const getPrefetchSchedule = yield* MediaTailor.GetPrefetchSchedule(config);
39 const deletePrefetchSchedule =
40 yield* MediaTailor.DeletePrefetchSchedule(config);
41 const listPrefetchSchedules =
42 yield* MediaTailor.ListPrefetchSchedules(config);
43 const listAlerts = yield* MediaTailor.ListAlerts();
44 const getChannelSchedule = yield* MediaTailor.GetChannelSchedule();
45 const startChannel = yield* MediaTailor.StartChannel();
46 const stopChannel = yield* MediaTailor.StopChannel();
47 const createProgram = yield* MediaTailor.CreateProgram();
48 const describeProgram = yield* MediaTailor.DescribeProgram();
49 const updateProgram = yield* MediaTailor.UpdateProgram();
50 const deleteProgram = yield* MediaTailor.DeleteProgram();
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 === "/health") {
59 return yield* HttpServerResponse.json({ ok: true });
60 }
61
62 if (request.method === "POST" && pathname === "/prefetch/create") {
63 const body = (yield* request.json) as unknown as { name: string };
64 const now = yield* Effect.sync(() => Date.now());
65 const result = yield* createPrefetchSchedule({
66 Name: body.name,
67 Retrieval: { EndTime: new Date(now + 60 * 60 * 1000) },
68 Consumption: { EndTime: new Date(now + 2 * 60 * 60 * 1000) },
69 }).pipe(
70 Effect.map((r) => ({
71 arn: r.Arn,
72 error: undefined,
73 detail: undefined,
74 })),
75 Effect.catch((e) =>
76 Effect.succeed({
77 arn: undefined,
78 error: e._tag,
79 detail: String(e),
80 }),
81 ),
82 );
83 return yield* HttpServerResponse.json(result);
84 }
85
86 if (request.method === "GET" && pathname === "/prefetch/get") {
87 const name = url.searchParams.get("name")!;
88 const result = yield* getPrefetchSchedule({ Name: name }).pipe(
89 Effect.map((r) => ({
90 name: r.Name,
91 error: undefined,
92 detail: undefined,
93 })),
94 Effect.catch((e) =>
95 Effect.succeed({
96 name: undefined,
97 error: e._tag,
98 detail: String(e),
99 }),
100 ),
101 );
102 return yield* HttpServerResponse.json(result);
103 }
104
105 if (request.method === "GET" && pathname === "/prefetch/list") {
106 const result = yield* listPrefetchSchedules({}).pipe(
107 Effect.map((r) => ({
108 names: (r.Items ?? []).map((item) => item.Name),
109 error: undefined,
110 detail: undefined,
111 })),
112 Effect.catch((e) =>
113 Effect.succeed({
114 names: [] as string[],
115 error: e._tag,
116 detail: String(e),
117 }),
118 ),
119 );
120 return yield* HttpServerResponse.json(result);
121 }
122
123 if (request.method === "POST" && pathname === "/prefetch/delete") {
124 const body = (yield* request.json) as unknown as { name: string };
125 const result = yield* deletePrefetchSchedule({
126 Name: body.name,
127 }).pipe(
128 Effect.map(() => ({
129 deleted: true,
130 error: undefined,
131 detail: undefined,
132 })),
133 Effect.catch((e) =>
134 Effect.succeed({
135 deleted: false,
136 error: e._tag,
137 detail: String(e),
138 }),
139 ),
140 );
141 return yield* HttpServerResponse.json(result);
142 }
143
144 if (request.method === "GET" && pathname === "/alerts") {
145 const arn = url.searchParams.get("arn")!;
146 const result = yield* listAlerts({ ResourceArn: arn }).pipe(
147 Effect.map((r) => ({
148 count: r.Items?.length ?? 0,
149 error: undefined,
150 detail: undefined,
151 })),
152 Effect.catch((e) =>
153 Effect.succeed({ count: 0, error: e._tag, detail: String(e) }),
154 ),
155 );
156 return yield* HttpServerResponse.json(result);
157 }
158
159 if (request.method === "GET" && pathname === "/channel/schedule") {
160 const name = url.searchParams.get("name")!;
161 const result = yield* getChannelSchedule({ ChannelName: name }).pipe(
162 Effect.map((r) => ({
163 count: r.Items?.length ?? 0,
164 error: undefined,
165 detail: undefined,
166 })),
167 Effect.catch((e) =>
168 Effect.succeed({ count: 0, error: e._tag, detail: String(e) }),
169 ),
170 );
171 return yield* HttpServerResponse.json(result);
172 }
173
174 if (request.method === "POST" && pathname === "/channel/start") {
175 const body = (yield* request.json) as unknown as { name: string };
176 const result = yield* startChannel({ ChannelName: body.name }).pipe(
177 Effect.map(() => ({
178 started: true,
179 error: undefined,
180 detail: undefined,
181 })),
182 Effect.catch((e) =>
183 Effect.succeed({
184 started: false,
185 error: e._tag,
186 detail: String(e),
187 }),
188 ),
189 );
190 return yield* HttpServerResponse.json(result);
191 }
192
193 if (request.method === "POST" && pathname === "/channel/stop") {
194 const body = (yield* request.json) as unknown as { name: string };
195 const result = yield* stopChannel({ ChannelName: body.name }).pipe(
196 Effect.map(() => ({
197 stopped: true,
198 error: undefined,
199 detail: undefined,
200 })),
201 Effect.catch((e) =>
202 Effect.succeed({
203 stopped: false,
204 error: e._tag,
205 detail: String(e),
206 }),
207 ),
208 );
209 return yield* HttpServerResponse.json(result);
210 }
211
212 if (request.method === "POST" && pathname === "/program/create") {
213 const result = yield* createProgram({
214 ChannelName: NONEXISTENT_CHANNEL,
215 ProgramName: NONEXISTENT_PROGRAM,
216 SourceLocationName: "alchemy-nonexistent-source-location",
217 VodSourceName: "alchemy-nonexistent-vod-source",
218 ScheduleConfiguration: {
219 Transition: {
220 Type: "RELATIVE",
221 RelativePosition: "AFTER_PROGRAM",
222 },
223 },
224 }).pipe(
225 Effect.map(() => ({
226 created: true,
227 error: undefined,
228 detail: undefined,
229 })),
230 Effect.catch((e) =>
231 Effect.succeed({
232 created: false,
233 error: e._tag,
234 detail: String(e),
235 }),
236 ),
237 );
238 return yield* HttpServerResponse.json(result);
239 }
240
241 if (request.method === "GET" && pathname === "/program") {
242 const result = yield* describeProgram({
243 ChannelName: NONEXISTENT_CHANNEL,
244 ProgramName: NONEXISTENT_PROGRAM,
245 }).pipe(
246 Effect.map((r) => ({
247 name: r.ProgramName,
248 error: undefined,
249 detail: undefined,
250 })),
251 Effect.catch((e) =>
252 Effect.succeed({
253 name: undefined,
254 error: e._tag,
255 detail: String(e),
256 }),
257 ),
258 );
259 return yield* HttpServerResponse.json(result);
260 }
261
262 if (request.method === "POST" && pathname === "/program/update") {
263 const result = yield* updateProgram({
264 ChannelName: NONEXISTENT_CHANNEL,
265 ProgramName: NONEXISTENT_PROGRAM,
266 ScheduleConfiguration: {},
267 }).pipe(
268 Effect.map(() => ({
269 updated: true,
270 error: undefined,
271 detail: undefined,
272 })),
273 Effect.catch((e) =>
274 Effect.succeed({
275 updated: false,
276 error: e._tag,
277 detail: String(e),
278 }),
279 ),
280 );
281 return yield* HttpServerResponse.json(result);
282 }
283
284 if (request.method === "POST" && pathname === "/program/delete") {
285 const result = yield* deleteProgram({
286 ChannelName: NONEXISTENT_CHANNEL,
287 ProgramName: NONEXISTENT_PROGRAM,
288 }).pipe(
289 Effect.map(() => ({
290 deleted: true,
291 error: undefined,
292 detail: undefined,
293 })),
294 Effect.catch((e) =>
295 Effect.succeed({
296 deleted: false,
297 error: e._tag,
298 detail: String(e),
299 }),
300 ),
301 );
302 return yield* HttpServerResponse.json(result);
303 }
304
305 return yield* HttpServerResponse.json(
306 { error: "Not found", method: request.method, pathname },
307 { status: 404 },
308 );
309 }).pipe(Effect.orDie),
310 };
311 }).pipe(
312 Effect.provide(
313 Layer.mergeAll(
314 MediaTailor.CreatePrefetchScheduleHttp,
315 MediaTailor.GetPrefetchScheduleHttp,
316 MediaTailor.DeletePrefetchScheduleHttp,
317 MediaTailor.ListPrefetchSchedulesHttp,
318 MediaTailor.ListAlertsHttp,
319 MediaTailor.GetChannelScheduleHttp,
320 MediaTailor.StartChannelHttp,
321 MediaTailor.StopChannelHttp,
322 MediaTailor.CreateProgramHttp,
323 MediaTailor.DescribeProgramHttp,
324 MediaTailor.UpdateProgramHttp,
325 MediaTailor.DeleteProgramHttp,
326 ),
327 ),
328 ),
329);