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 { Crawler } from "./Crawler.ts";
6import type { Database } from "./Database.ts";
7import type { Job } from "./Job.ts";
8import type { Table } from "./Table.ts";
9
10/**
11 * Shared scaffolding for AWS Glue HTTP bindings.
12 *
13 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
14 * a `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders
15 * below. Everything except the operation, the identifier injection, and the
16 * IAM action list is boilerplate.
17 *
18 * Glue's catalog IAM model authorizes table- and partition-addressed actions
19 * against the *catalog*, *database*, and *table* ARNs together, so the
20 * database/table builders grant on all the ARNs the action evaluates.
21 */
22
23/**
24 * Build the impl Effect for a Glue operation addressed to a {@link Job} by
25 * `JobName`: the runtime callable injects the bound job's name and the
26 * deploy-time half grants `actions` on the job ARN — or on `*` when
27 * `anyResource` is set, for the handful of Glue actions that do not support
28 * resource-level permissions (verified live: `glue:GetJobBookmark` /
29 * `glue:ResetJobBookmark` are evaluated with no resource — an ARN-scoped
30 * Allow never matches and IAM implicit-denies).
31 */
32export const makeGlueJobHttpBinding = <
33 I extends { JobName: string },
34 A,
35 E,
36 R,
37>(options: {
38 /** Fully-qualified binding tag, e.g. `AWS.Glue.GetJobRun`. */
39 tag: string;
40 /** The distilled operation; `JobName` is injected from the job. */
41 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
42 /** IAM actions granted on the job ARN. */
43 actions: readonly string[];
44 /**
45 * Grant on `Resource: "*"` instead of the job ARN — required for actions
46 * Glue evaluates without a resource (job bookmarks).
47 */
48 anyResource?: boolean;
49}) =>
50 Effect.gen(function* () {
51 const op = yield* options.operation;
52
53 return Effect.fn(function* (job: Job) {
54 // Outputs yield a DEFERRED effect — resolve again per invocation below.
55 const JobName = yield* job.jobName;
56 if (!globalThis.__ALCHEMY_RUNTIME__) {
57 const host = yield* Binding.Host;
58 if (isBindingHost(host)) {
59 yield* host.bind`Allow(${host}, ${options.tag}(${job}))`({
60 policyStatements: [
61 {
62 Effect: "Allow",
63 Action: [...options.actions],
64 Resource: options.anyResource ? ["*"] : [job.jobArn],
65 },
66 ],
67 });
68 }
69 }
70 return Effect.fn(`${options.tag}(${job.LogicalId})`)(function* (
71 request?: Omit<I, "JobName">,
72 ) {
73 return yield* op({ ...request, JobName: yield* JobName } as I);
74 });
75 });
76 });
77
78/**
79 * Build the impl Effect for a Glue operation addressed to a {@link Crawler}
80 * by `Name`: the runtime callable injects the bound crawler's name and the
81 * deploy-time half grants `actions` on the crawler ARN.
82 */
83export const makeGlueCrawlerHttpBinding = <
84 I extends { Name: string },
85 A,
86 E,
87 R,
88>(options: {
89 /** Fully-qualified binding tag, e.g. `AWS.Glue.StartCrawler`. */
90 tag: string;
91 /** The distilled operation; `Name` is injected from the crawler. */
92 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
93 /** IAM actions granted on the crawler ARN. */
94 actions: readonly string[];
95}) =>
96 Effect.gen(function* () {
97 const op = yield* options.operation;
98
99 return Effect.fn(function* (crawler: Crawler) {
100 const Name = yield* crawler.crawlerName;
101 if (!globalThis.__ALCHEMY_RUNTIME__) {
102 const host = yield* Binding.Host;
103 if (isBindingHost(host)) {
104 yield* host.bind`Allow(${host}, ${options.tag}(${crawler}))`({
105 policyStatements: [
106 {
107 Effect: "Allow",
108 Action: [...options.actions],
109 Resource: [crawler.crawlerArn],
110 },
111 ],
112 });
113 }
114 }
115 return Effect.fn(`${options.tag}(${crawler.LogicalId})`)(function* (
116 request?: Omit<I, "Name">,
117 ) {
118 return yield* op({ ...request, Name: yield* Name } as I);
119 });
120 });
121 });
122
123/**
124 * Build the impl Effect for a Glue Data Catalog operation addressed to a
125 * {@link Database}: the runtime callable injects `DatabaseName` (+
126 * `CatalogId`) and the deploy-time half grants `actions` on the catalog,
127 * database, and contained-tables ARNs (Glue evaluates catalog actions
128 * against all three).
129 */
130export const makeGlueDatabaseHttpBinding = <
131 I extends { DatabaseName: string; CatalogId?: string },
132 A,
133 E,
134 R,
135>(options: {
136 /** Fully-qualified binding tag, e.g. `AWS.Glue.GetTables`. */
137 tag: string;
138 /** The distilled operation; `DatabaseName`/`CatalogId` are injected. */
139 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
140 /** IAM actions granted on the catalog + database + tables ARNs. */
141 actions: readonly string[];
142}) =>
143 Effect.gen(function* () {
144 const op = yield* options.operation;
145
146 return Effect.fn(function* (database: Database) {
147 const DatabaseName = yield* database.databaseName;
148 const CatalogId = yield* database.catalogId;
149 if (!globalThis.__ALCHEMY_RUNTIME__) {
150 const host = yield* Binding.Host;
151 if (isBindingHost(host)) {
152 yield* host.bind`Allow(${host}, ${options.tag}(${database}))`({
153 policyStatements: [
154 {
155 Effect: "Allow",
156 Action: [...options.actions],
157 Resource: [
158 // arn:…:database/{db} → arn:…:catalog
159 Output.map(database.databaseArn, (arn) =>
160 arn.replace(/:database\/.*$/, ":catalog"),
161 ),
162 database.databaseArn,
163 // arn:…:database/{db} → arn:…:table/{db}/*
164 Output.map(
165 database.databaseArn,
166 (arn) => `${arn.replace(":database/", ":table/")}/*`,
167 ),
168 ],
169 },
170 ],
171 });
172 }
173 }
174 return Effect.fn(`${options.tag}(${database.LogicalId})`)(function* (
175 request?: Omit<I, "DatabaseName" | "CatalogId">,
176 ) {
177 return yield* op({
178 ...request,
179 DatabaseName: yield* DatabaseName,
180 CatalogId: yield* CatalogId,
181 } as I);
182 });
183 });
184 });
185
186/**
187 * Build the impl Effect for a Glue Data Catalog operation addressed to a
188 * {@link Table}: the runtime callable injects `DatabaseName` + the table
189 * name (+ `CatalogId`) and the deploy-time half grants `actions` on the
190 * catalog, database, and table ARNs. Most table-addressed inputs carry the
191 * table name as `TableName`; `GetTable` uses `Name` — pick with
192 * `tableNameKey`.
193 */
194export const makeGlueTableHttpBinding = <
195 K extends "TableName" | "Name",
196 I extends { DatabaseName: string; CatalogId?: string } & {
197 [key in K]: string;
198 },
199 A,
200 E,
201 R,
202>(options: {
203 /** Fully-qualified binding tag, e.g. `AWS.Glue.GetPartitions`. */
204 tag: string;
205 /** The distilled operation; database/table identifiers are injected. */
206 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
207 /** IAM actions granted on the catalog + database + table ARNs. */
208 actions: readonly string[];
209 /** The request key carrying the table name. */
210 tableNameKey: K;
211}) =>
212 Effect.gen(function* () {
213 const op = yield* options.operation;
214
215 return Effect.fn(function* (table: Table) {
216 const DatabaseName = yield* table.databaseName;
217 const TableName = yield* table.tableName;
218 const CatalogId = yield* table.catalogId;
219 if (!globalThis.__ALCHEMY_RUNTIME__) {
220 const host = yield* Binding.Host;
221 if (isBindingHost(host)) {
222 yield* host.bind`Allow(${host}, ${options.tag}(${table}))`({
223 policyStatements: [
224 {
225 Effect: "Allow",
226 Action: [...options.actions],
227 Resource: [
228 // arn:…:table/{db}/{tbl} → arn:…:catalog
229 Output.map(table.tableArn, (arn) =>
230 arn.replace(/:table\/.*$/, ":catalog"),
231 ),
232 // arn:…:table/{db}/{tbl} → arn:…:database/{db}
233 Output.map(table.tableArn, (arn) =>
234 arn.replace(/:table\/([^/]+)\/.*$/, ":database/$1"),
235 ),
236 table.tableArn,
237 ],
238 },
239 ],
240 });
241 }
242 }
243 return Effect.fn(`${options.tag}(${table.LogicalId})`)(function* (
244 request?: Omit<I, "DatabaseName" | "CatalogId" | K>,
245 ) {
246 return yield* op({
247 ...request,
248 DatabaseName: yield* DatabaseName,
249 [options.tableNameKey]: yield* TableName,
250 CatalogId: yield* CatalogId,
251 } as I);
252 });
253 });
254 });