1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { JobQueue } from "./JobQueue.ts";
6
7/**
8 * Shared scaffolding for AWS Batch HTTP bindings.
9 *
10 * NOT exported from `index.ts` — every queue-anchored `{Op}Http.ts` in this
11 * service is a thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of
12 * the two builders below. Everything except the operation, the IAM action
13 * list, and (for queue-scoped operations) the injected `jobQueue` is
14 * boilerplate. `SubmitJobHttp` binds TWO resources (queue + job definition)
15 * and injects both, so it stays bespoke.
16 */
17
18/**
19 * Build the impl Effect for a job-level operation (describe/terminate/cancel
20 * submitted jobs). The bound {@link JobQueue} anchors the binding's identity;
21 * the deploy-time half grants `actions` on `*` because job ARNs
22 * (`arn:…:job/{jobId}`) are only known at runtime and `batch:DescribeJobs` /
23 * `batch:ListJobs` have no resource-level IAM at all.
24 */
25export const makeBatchJobHttpBinding = <I, A, E, R>(options: {
26 /** Fully-qualified binding tag, e.g. `AWS.Batch.DescribeJobs`. */
27 tag: string;
28 /** The distilled operation, invoked with the caller's request as-is. */
29 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
30 /** IAM actions granted on `*`. */
31 actions: readonly string[];
32}) =>
33 Effect.gen(function* () {
34 const op = yield* options.operation;
35
36 return Effect.fn(function* (queue: JobQueue) {
37 if (!globalThis.__ALCHEMY_RUNTIME__) {
38 const host = yield* Binding.Host;
39 if (isBindingHost(host)) {
40 yield* host.bind`Allow(${host}, ${options.tag}(${queue}))`({
41 policyStatements: [
42 {
43 Effect: "Allow",
44 Action: [...options.actions],
45 Resource: ["*"],
46 },
47 ],
48 });
49 }
50 }
51 return Effect.fn(`${options.tag}(${queue.LogicalId})`)(function* (
52 request: I,
53 ) {
54 return yield* op(request);
55 });
56 });
57 });
58
59/**
60 * Build the impl Effect for a queue-scoped operation: the runtime callable
61 * injects the bound {@link JobQueue}'s ARN as `jobQueue` and the deploy-time
62 * half grants `actions` on the queue ARN (or `*` when the action has no
63 * resource-level IAM).
64 */
65export const makeBatchQueueHttpBinding = <
66 I extends { jobQueue?: string },
67 A,
68 E,
69 R,
70>(options: {
71 /** Fully-qualified binding tag, e.g. `AWS.Batch.ListJobs`. */
72 tag: string;
73 /** The distilled operation; `jobQueue` is injected from the bound queue. */
74 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
75 /** IAM actions granted on the queue ARN (or `*` — see `wildcardIam`). */
76 actions: readonly string[];
77 /** Grant on `*` instead of the queue ARN (actions without resource-level IAM). */
78 wildcardIam?: boolean;
79}) =>
80 Effect.gen(function* () {
81 const op = yield* options.operation;
82
83 return Effect.fn(function* (queue: JobQueue) {
84 // Outputs yield a DEFERRED effect — resolve again per invocation below.
85 const JobQueueArn = yield* queue.jobQueueArn;
86 if (!globalThis.__ALCHEMY_RUNTIME__) {
87 const host = yield* Binding.Host;
88 if (isBindingHost(host)) {
89 yield* host.bind`Allow(${host}, ${options.tag}(${queue}))`({
90 policyStatements: [
91 {
92 Effect: "Allow",
93 Action: [...options.actions],
94 Resource: options.wildcardIam
95 ? ["*"]
96 : [Output.interpolate`${queue.jobQueueArn}`],
97 },
98 ],
99 });
100 }
101 }
102 return Effect.fn(`${options.tag}(${queue.LogicalId})`)(function* (
103 request?: Omit<I, "jobQueue">,
104 ) {
105 return yield* op({
106 ...request,
107 jobQueue: yield* JobQueueArn,
108 } as I);
109 });
110 });
111 });