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";
6import { connectEnvPrefix } from "./Connect.ts";
7
8/**
9 * Shared scaffolding for the DAX connect bindings.
10 *
11 * NOT exported from `index.ts` — `ConnectReadHttp.ts` / `ConnectWriteHttp.ts`
12 * / `ConnectReadWriteHttp.ts` are thin
13 * `Layer.effect(Cap, makeDaxConnectHttpBinding({ … }))` calls over the
14 * builder below. Only the IAM action list differs per access level.
15 */
16
17/**
18 * Protocol actions every DAX client needs regardless of access level — the
19 * client discovers cluster nodes and negotiates the item schema before any
20 * item operation.
21 */
22export const DAX_PROTOCOL_ACTIONS = [
23 "dax:DefineAttributeList",
24 "dax:DefineAttributeListId",
25 "dax:DefineKeySchema",
26 "dax:Endpoints",
27] as const;
28
29/** Read-side DAX data-plane actions. */
30export const DAX_READ_ACTIONS = [
31 "dax:GetItem",
32 "dax:BatchGetItem",
33 "dax:Query",
34 "dax:Scan",
35] as const;
36
37/** Write-side DAX data-plane actions. */
38export const DAX_WRITE_ACTIONS = [
39 "dax:PutItem",
40 "dax:UpdateItem",
41 "dax:DeleteItem",
42 "dax:BatchWriteItem",
43 "dax:ConditionCheckItem",
44] as const;
45
46/**
47 * Build the impl Effect for a connect binding. At deploy time it grants
48 * `actions` on the cluster ARN and publishes the discovery endpoint as
49 * `DAX_{LOGICAL_ID}_{HOST,PORT,URL,TLS}` environment variables on the host
50 * Function; at runtime it resolves the same values into a typed connection
51 * descriptor.
52 */
53export const makeDaxConnectHttpBinding = (options: {
54 /** Fully-qualified binding tag, e.g. `AWS.DAX.ConnectReadWrite`. */
55 tag: string;
56 /** IAM actions granted on the cluster ARN. */
57 actions: readonly string[];
58}) =>
59 Effect.gen(function* () {
60 return Effect.fn(function* (cluster: Cluster) {
61 const Host = yield* cluster.discoveryEndpointAddress;
62 const Port = yield* cluster.discoveryEndpointPort;
63 const Url = yield* cluster.discoveryEndpointUrl;
64 const EncryptionType = yield* cluster.clusterEndpointEncryptionType;
65
66 if (!globalThis.__ALCHEMY_RUNTIME__) {
67 const host = yield* Binding.Host;
68 if (isBindingHost(host)) {
69 const prefix = connectEnvPrefix(cluster.LogicalId);
70 yield* host.bind`Allow(${host}, ${options.tag}(${cluster}))`({
71 env: {
72 [`${prefix}_HOST`]: cluster.discoveryEndpointAddress,
73 // Lambda environment variables are strings — stringify the port.
74 [`${prefix}_PORT`]: Output.interpolate`${cluster.discoveryEndpointPort}`,
75 [`${prefix}_URL`]: cluster.discoveryEndpointUrl,
76 [`${prefix}_TLS`]: Output.map(
77 cluster.clusterEndpointEncryptionType,
78 (encryption) => (encryption === "TLS" ? "true" : "false"),
79 ),
80 },
81 policyStatements: [
82 {
83 Effect: "Allow",
84 Action: [...options.actions],
85 Resource: [Output.interpolate`${cluster.clusterArn}`],
86 },
87 ],
88 });
89 }
90 }
91
92 return Effect.gen(function* () {
93 const host = yield* Host;
94 const port = yield* Port;
95 const url = yield* Url;
96 const encryptionType = yield* EncryptionType;
97 if (!host || port === undefined || !url) {
98 return yield* Effect.die(
99 `DAX discovery endpoint for '${cluster.LogicalId}' is not available yet`,
100 );
101 }
102 return {
103 host,
104 port,
105 url,
106 tls: encryptionType === "TLS",
107 };
108 });
109 });
110 });