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 { Cluster } from "./Cluster.ts";
6
7/**
8 * Shared scaffolding for Amazon EMR HTTP bindings.
9 *
10 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
11 * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the two
12 * builders below. Everything except the operation, the IAM action list, and
13 * the injected cluster identifier is boilerplate.
14 */
15
16/**
17 * Build the impl Effect for a cluster-scoped operation. The deploy-time half
18 * grants `actions` on the bound {@link Cluster}'s ARN (EMR authorizes
19 * cluster-addressed actions against the cluster ARN); the runtime callable
20 * injects the cluster's identifier into the request as `inject`:
21 *
22 * - `"ClusterId"` (default) / `"JobFlowId"` — the cluster id (`j-…`)
23 * - `"TargetResourceArn"` — the cluster ARN (persistent app UIs)
24 * - `"none"` — pass the caller's request through unchanged (id-addressed
25 * companions like `DescribePersistentAppUI` that EMR still authorizes
26 * against the cluster the target belongs to)
27 */
28export const makeEmrClusterHttpBinding = <I, A, E, R>(options: {
29 /** Fully-qualified binding tag, e.g. `AWS.EMR.DescribeStep`. */
30 tag: string;
31 /** The distilled operation. */
32 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
33 /** IAM actions granted on the cluster ARN. */
34 actions: readonly string[];
35 /** Request field the bound cluster is injected as. */
36 inject?: "ClusterId" | "JobFlowId" | "TargetResourceArn" | "none";
37}) =>
38 Effect.gen(function* () {
39 const op = yield* options.operation;
40 const inject = options.inject ?? "ClusterId";
41
42 return Effect.fn(function* <C extends Cluster>(cluster: C) {
43 // Outputs yield a DEFERRED effect — resolve again per invocation below.
44 const Injected = yield* inject === "TargetResourceArn"
45 ? cluster.clusterArn
46 : cluster.clusterId;
47 if (!globalThis.__ALCHEMY_RUNTIME__) {
48 const host = yield* Binding.Host;
49 if (isBindingHost(host)) {
50 yield* host.bind`Allow(${host}, ${options.tag}(${cluster}))`({
51 policyStatements: [
52 {
53 Effect: "Allow",
54 Action: [...options.actions],
55 Resource: [Output.interpolate`${cluster.clusterArn}`],
56 },
57 ],
58 });
59 }
60 }
61 return Effect.fn(`${options.tag}(${cluster.LogicalId})`)(function* (
62 request?: Omit<I, "ClusterId" | "JobFlowId" | "TargetResourceArn">,
63 ) {
64 return yield* op(
65 (inject === "none"
66 ? { ...request }
67 : { ...request, [inject]: yield* Injected }) as I,
68 );
69 });
70 });
71 });
72
73/**
74 * Build the impl Effect for an account-level operation (cluster inventory,
75 * release-label catalog). The deploy-time half grants `actions` on `*` —
76 * these read-only discovery actions span every cluster/release label in the
77 * account and support no resource-level scoping.
78 */
79export const makeEmrAccountHttpBinding = <I, A, E, R>(options: {
80 /** Fully-qualified binding tag, e.g. `AWS.EMR.ListClusters`. */
81 tag: string;
82 /** The distilled operation, invoked with the caller's request as-is. */
83 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
84 /** IAM actions granted on `*`. */
85 actions: readonly string[];
86}) =>
87 Effect.gen(function* () {
88 const op = yield* options.operation;
89
90 return Effect.fn(function* () {
91 if (!globalThis.__ALCHEMY_RUNTIME__) {
92 const host = yield* Binding.Host;
93 if (isBindingHost(host)) {
94 yield* host.bind`Allow(${host}, ${options.tag}())`({
95 policyStatements: [
96 {
97 Effect: "Allow",
98 Action: [...options.actions],
99 Resource: ["*"],
100 },
101 ],
102 });
103 }
104 }
105 return Effect.fn(options.tag)(function* (request?: I) {
106 return yield* op((request ?? {}) as I);
107 });
108 });
109 });