1import * as serverless from "@distilled.cloud/aws/redshift-serverless";
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 { Connect, connectEnvPrefix, type ConnectOptions } from "./Connect.ts";
11import type { Workgroup } from "./Workgroup.ts";
12
13const DEFAULT_PORT = 5439;
14const DEFAULT_DATABASE = "dev";
15
16const unwrap = (
17 value: string | Redacted.Redacted<string> | undefined,
18): string | undefined =>
19 value === undefined
20 ? undefined
21 : Redacted.isRedacted(value)
22 ? Redacted.value(value)
23 : value;
24
25/**
26 * SDK-backed implementation of {@link Connect}. Deploy half attaches the
27 * `redshift-serverless:GetCredentials` policy on the workgroup ARN and
28 * publishes `REDSHIFT_SERVERLESS_{LOGICAL_ID}_{HOST,PORT}` on the host
29 * Function; runtime half mints temporary credentials via
30 * `redshift-serverless:GetCredentials` and formats a pgwire connection URL.
31 */
32export const ConnectHttp = Layer.effect(
33 Connect,
34 Effect.gen(function* () {
35 const getCredentials = yield* serverless.getCredentials;
36
37 return Effect.fn(function* (
38 workgroup: Workgroup,
39 options: ConnectOptions = {},
40 ) {
41 const WorkgroupName = yield* workgroup.workgroupName;
42 const Host = yield* workgroup.endpointAddress;
43 const Port = yield* workgroup.endpointPort;
44
45 if (!globalThis.__ALCHEMY_RUNTIME__) {
46 const host = yield* Binding.Host;
47 if (isBindingHost(host)) {
48 const prefix = connectEnvPrefix(workgroup.LogicalId);
49 yield* host.bind`Allow(${host}, AWS.RedshiftServerless.Connect(${workgroup}))`(
50 {
51 policyStatements: [
52 {
53 Effect: "Allow",
54 Action: ["redshift-serverless:GetCredentials"],
55 Resource: [workgroup.workgroupArn],
56 },
57 ],
58 env: {
59 [`${prefix}_HOST`]: workgroup.endpointAddress,
60 // Lambda environment variables are strings — stringify the port.
61 [`${prefix}_PORT`]: Output.interpolate`${workgroup.endpointPort}`,
62 },
63 },
64 );
65 }
66 }
67
68 const database = options.database ?? DEFAULT_DATABASE;
69
70 return Effect.gen(function* () {
71 const workgroupName = yield* WorkgroupName;
72 const host = yield* Host;
73 const port = (yield* Port) ?? DEFAULT_PORT;
74 if (!host) {
75 return yield* Effect.die(
76 `Redshift Serverless workgroup endpoint for '${workgroup.LogicalId}' is not available yet`,
77 );
78 }
79 const credentials = yield* getCredentials({
80 workgroupName,
81 dbName: database,
82 durationSeconds: toWireSeconds(options.duration),
83 });
84 const username = unwrap(credentials.dbUser);
85 const rawPassword = unwrap(credentials.dbPassword);
86 const password =
87 rawPassword === undefined ? undefined : Redacted.make(rawPassword);
88 return {
89 host,
90 port,
91 database,
92 username,
93 password,
94 // Redshift Serverless endpoints require TLS.
95 ssl: true,
96 expiration: credentials.expiration,
97 url: formatSqlConnectionUrl({
98 host,
99 port,
100 database,
101 username,
102 password,
103 ssl: true,
104 }),
105 };
106 });
107 });
108 }),
109);