1/**
2 * Shared scaffolding for RDS Data API HTTP bindings.
3 *
4 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
5 * a `Layer.effect(Cap, makeRDSDataHttpBinding({ … }))`. Every Data API
6 * operation follows the same shape: it is scoped to a Data-API-enabled
7 * {@link DBCluster} plus the Secrets Manager {@link Secret} holding the
8 * database credentials, the deploy-time half grants the `rds-data:*` action
9 * on the cluster + secret ARNs (plus `secretsmanager:GetSecretValue` /
10 * `DescribeSecret` on the secret, which the Data API resolves server-side),
11 * and the runtime half injects the resolved ARNs (and optional
12 * `database`/`schema`) into the wire request. Everything except the
13 * operation, the IAM action, and the request shaping is boilerplate.
14 */
15import * as Effect from "effect/Effect";
16import * as Binding from "../../Binding.ts";
17import { isBindingHost } from "../Lambda/Function.ts";
18import type { DBCluster } from "../RDS/DBCluster.ts";
19import type { Secret } from "../SecretsManager/Secret.ts";
20
21/**
22 * The common bind-time options every RDS Data API capability accepts: the
23 * credentials secret, plus the optional default `database`/`schema` injected
24 * into requests by operations that support them.
25 */
26export interface RDSDataBindingOptions {
27 secret: Secret;
28 database?: string;
29 schema?: string;
30}
31
32/**
33 * Build the impl Effect for an RDS Data API operation scoped to a
34 * {@link DBCluster} + credentials {@link Secret}: the deploy-time half grants
35 * `action` on the cluster and secret ARNs (plus Secrets Manager read on the
36 * secret), and the runtime half shapes the wire request via `makeInput` with
37 * the resolved ARNs and bind-time `database`/`schema`.
38 */
39export const makeRDSDataHttpBinding = <Req, I, A, E, R>(options: {
40 /** Fully-qualified binding tag, e.g. `AWS.RDSData.ExecuteStatement`. */
41 tag: string;
42 /** IAM action granted on the cluster + secret ARNs, e.g. `rds-data:ExecuteStatement`. */
43 action: string;
44 /** The distilled operation invoked with the shaped wire request. */
45 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
46 /**
47 * Shape the wire request from the caller's request plus the resolved
48 * cluster/secret ARNs and the bind-time `database`/`schema`.
49 */
50 makeInput: (
51 request: Req,
52 context: {
53 resourceArn: string;
54 secretArn: string;
55 database: string | undefined;
56 schema: string | undefined;
57 },
58 ) => I;
59}) =>
60 Effect.gen(function* () {
61 const op = yield* options.operation;
62
63 return Effect.fn(function* (
64 cluster: DBCluster,
65 bindOptions: RDSDataBindingOptions,
66 ) {
67 const resourceArn = yield* cluster.dbClusterArn;
68 const secretArn = yield* bindOptions.secret.secretArn;
69 if (!globalThis.__ALCHEMY_RUNTIME__) {
70 const host = yield* Binding.Host;
71 if (isBindingHost(host)) {
72 yield* host.bind`Allow(${host}, ${options.tag}(${cluster}, ${bindOptions.secret}))`(
73 {
74 policyStatements: [
75 {
76 Effect: "Allow",
77 Action: [options.action],
78 Resource: [
79 cluster.dbClusterArn,
80 bindOptions.secret.secretArn,
81 ],
82 },
83 {
84 Effect: "Allow",
85 Action: [
86 "secretsmanager:GetSecretValue",
87 "secretsmanager:DescribeSecret",
88 ],
89 Resource: [bindOptions.secret.secretArn],
90 },
91 ],
92 },
93 );
94 }
95 }
96 return Effect.fn(`${options.tag}(${cluster.LogicalId})`)(function* (
97 // optional so zero-arg callables (`BeginTransaction`) conform to
98 // their declared `() => Effect<…>` interface; `makeInput` receives
99 // `undefined` there and builds the request from context alone.
100 request?: Req,
101 ) {
102 return yield* op(
103 options.makeInput(request as Req, {
104 resourceArn: yield* resourceArn,
105 secretArn: yield* secretArn,
106 database: bindOptions.database,
107 schema: bindOptions.schema,
108 }),
109 );
110 });
111 });
112 });