1import * as redshift from "@distilled.cloud/aws/redshift";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Redacted from "effect/Redacted";
5import * as Binding from "../../Binding.ts";
6import * as Output from "../../Output.ts";
7import { toWireSeconds } from "../../Util/Duration.ts";
8import { formatSqlConnectionUrl } from "../Connection/internal.ts";
9import { isBindingHost } from "../Lambda/Function.ts";
10import type { Cluster } from "./Cluster.ts";
11import { Connect, connectEnvPrefix, type ConnectOptions } from "./Connect.ts";
12
13const DEFAULT_PORT = 5439;
14
15const unwrap = (
16 value: string | Redacted.Redacted<string> | undefined,
17): string | undefined =>
18 value === undefined
19 ? undefined
20 : Redacted.isRedacted(value)
21 ? Redacted.value(value)
22 : value;
23
24/**
25 * SDK-backed implementation of {@link Connect}. Deploy half attaches the
26 * `redshift:GetClusterCredentials[WithIAM]` policy scoped to the cluster's
27 * `dbname`/`dbuser`/`dbgroup` ARNs and publishes
28 * `REDSHIFT_{LOGICAL_ID}_{HOST,PORT}` on the host Function; runtime half
29 * mints temporary credentials via the corresponding SDK operation and
30 * formats a pgwire connection URL.
31 */
32export const ConnectHttp = Layer.effect(
33 Connect,
34 Effect.gen(function* () {
35 const getClusterCredentials = yield* redshift.getClusterCredentials;
36 const getClusterCredentialsWithIAM =
37 yield* redshift.getClusterCredentialsWithIAM;
38
39 return Effect.fn(function* (
40 cluster: Cluster,
41 options: ConnectOptions = {},
42 ) {
43 const ClusterIdentifier = yield* cluster.clusterIdentifier;
44 const Host = yield* cluster.endpointAddress;
45 const Port = yield* cluster.endpointPort;
46 const DbName = yield* cluster.dbName;
47
48 if (!globalThis.__ALCHEMY_RUNTIME__) {
49 const host = yield* Binding.Host;
50 if (isBindingHost(host)) {
51 // `arn:aws:redshift:{region}:{acct}` — the cluster ARN minus its
52 // `:cluster:{id}` suffix; base for the dbname/dbuser/dbgroup ARNs
53 // that scope the GetClusterCredentials* actions.
54 const arnBase = Output.map(cluster.clusterArn, (arn) =>
55 arn.slice(0, arn.lastIndexOf(":cluster:")),
56 );
57 const database = options.database ?? cluster.dbName;
58 const dbNameArn = Output.interpolate`${arnBase}:dbname:${cluster.clusterIdentifier}/${database}`;
59 const prefix = connectEnvPrefix(cluster.LogicalId);
60 yield* host.bind`Allow(${host}, AWS.Redshift.Connect(${cluster}))`({
61 policyStatements:
62 options.dbUser !== undefined
63 ? [
64 {
65 Effect: "Allow" as const,
66 Action: [
67 "redshift:GetClusterCredentials",
68 ...(options.autoCreate
69 ? ["redshift:CreateClusterUser"]
70 : []),
71 ],
72 Resource: [
73 Output.interpolate`${arnBase}:dbuser:${cluster.clusterIdentifier}/${options.dbUser}`,
74 dbNameArn,
75 ],
76 },
77 ...(options.dbGroups && options.dbGroups.length > 0
78 ? [
79 {
80 Effect: "Allow" as const,
81 Action: ["redshift:JoinGroup"],
82 Resource: options.dbGroups.map(
83 (group) =>
84 Output.interpolate`${arnBase}:dbgroup:${cluster.clusterIdentifier}/${group}`,
85 ),
86 },
87 ]
88 : []),
89 ]
90 : [
91 {
92 Effect: "Allow" as const,
93 Action: ["redshift:GetClusterCredentialsWithIAM"],
94 Resource: [dbNameArn],
95 },
96 ],
97 env: {
98 [`${prefix}_HOST`]: cluster.endpointAddress,
99 // Lambda environment variables are strings — stringify the port.
100 [`${prefix}_PORT`]: Output.interpolate`${cluster.endpointPort}`,
101 },
102 });
103 }
104 }
105
106 return Effect.gen(function* () {
107 const clusterIdentifier = yield* ClusterIdentifier;
108 const host = yield* Host;
109 const port = (yield* Port) ?? DEFAULT_PORT;
110 const database = options.database ?? (yield* DbName);
111 if (!host) {
112 return yield* Effect.die(
113 `Redshift cluster endpoint for '${cluster.LogicalId}' is not available yet`,
114 );
115 }
116 const credentials =
117 options.dbUser !== undefined
118 ? yield* getClusterCredentials({
119 ClusterIdentifier: clusterIdentifier,
120 DbUser: options.dbUser,
121 DbName: database,
122 AutoCreate: options.autoCreate,
123 DbGroups: options.dbGroups,
124 DurationSeconds: toWireSeconds(options.duration),
125 })
126 : yield* getClusterCredentialsWithIAM({
127 ClusterIdentifier: clusterIdentifier,
128 DbName: database,
129 DurationSeconds: toWireSeconds(options.duration),
130 });
131 const username = unwrap(credentials.DbUser);
132 const rawPassword = unwrap(credentials.DbPassword);
133 const password =
134 rawPassword === undefined ? undefined : Redacted.make(rawPassword);
135 const ssl = options.ssl ?? true;
136 return {
137 host,
138 port,
139 database,
140 username,
141 password,
142 ssl,
143 expiration: credentials.Expiration,
144 url: formatSqlConnectionUrl({
145 host,
146 port,
147 database,
148 username,
149 password,
150 ssl,
151 }),
152 };
153 });
154 });
155 }),
156);