1import * as TSQ from "@distilled.cloud/aws/timestream-query";
2import * as TSW from "@distilled.cloud/aws/timestream-write";
3import * as Effect from "effect/Effect";
4import * as Binding from "../../Binding.ts";
5import * as Output from "../../Output.ts";
6import { isBindingHost } from "../Lambda/Function.ts";
7import type { Table } from "./Table.ts";
8import { discover, withEndpoint } from "./internal.ts";
9
10/**
11 * Shared scaffolding for Amazon Timestream 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 IAM action list, and the
16 * injected identifier is boilerplate.
17 *
18 * Timestream's wrinkle: every request must be routed through the
19 * cell-specific endpoint returned by `DescribeEndpoints` (see
20 * {@link discover}), so each builder captures its service's
21 * `describeEndpoints` at layer init (yield-first, so the runtime callable is
22 * requirement-free) and every binding additionally grants the unscoped
23 * `timestream:DescribeEndpoints` the discovery flow needs.
24 */
25
26const describeEndpointsStatement = {
27 Effect: "Allow" as const,
28 // Endpoint discovery is required and is not scoped to a resource.
29 Action: ["timestream:DescribeEndpoints"],
30 Resource: ["*"],
31};
32
33/**
34 * Build the impl Effect for a table-scoped `timestream-write` operation. The
35 * deploy-time half grants `actions` on the bound {@link Table}'s ARN (plus
36 * its owning database's ARN when `grantDatabaseArn` is set — batch load
37 * authorizes against both); the runtime callable shapes the caller's request
38 * via `toRequest`, injecting the table's physical names.
39 */
40export const makeWriteTableHttpBinding = <Req, I, A, E, R>(options: {
41 /** Fully-qualified binding tag, e.g. `AWS.Timestream.WriteRecords`. */
42 tag: string;
43 /** The distilled `timestream-write` operation. */
44 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
45 /** IAM actions granted on the table ARN. */
46 actions: readonly string[];
47 /** Also grant `actions` on the owning database's ARN (batch load). */
48 grantDatabaseArn?: boolean;
49 /** Shape the wire request from the caller's request + the table names. */
50 toRequest: (
51 request: Req,
52 names: { DatabaseName: string; TableName: string },
53 ) => I;
54}) =>
55 Effect.gen(function* () {
56 // Yield-first captures the operations' services (Credentials/Region/
57 // HttpClient) at layer init so the runtime callable is requirement-free.
58 const op = yield* options.operation;
59 const describeEndpoints = yield* TSW.describeEndpoints;
60 const withWriteEndpoint = withEndpoint(
61 discover("write", describeEndpoints({})),
62 );
63 return Effect.fn(function* (table: Table) {
64 const DatabaseName = yield* table.databaseName;
65 const TableName = yield* table.tableName;
66 if (!globalThis.__ALCHEMY_RUNTIME__) {
67 const host = yield* Binding.Host;
68 if (isBindingHost(host)) {
69 yield* host.bind`Allow(${host}, ${options.tag}(${table}))`({
70 policyStatements: [
71 {
72 Effect: "Allow",
73 Action: [...options.actions],
74 Resource: options.grantDatabaseArn
75 ? [
76 Output.interpolate`${table.tableArn}`,
77 // The owning database's ARN is the table ARN minus its
78 // `/table/{name}` suffix.
79 table.tableArn.pipe(
80 Output.map((arn: string) =>
81 arn.replace(/\/table\/[^/]*$/, ""),
82 ),
83 ),
84 ]
85 : [Output.interpolate`${table.tableArn}`],
86 },
87 describeEndpointsStatement,
88 ],
89 });
90 }
91 }
92 return Effect.fn(`${options.tag}(${table.LogicalId})`)(function* (
93 request: Req,
94 ) {
95 return yield* withWriteEndpoint(
96 op(
97 options.toRequest(request, {
98 DatabaseName: yield* DatabaseName,
99 TableName: yield* TableName,
100 }),
101 ),
102 );
103 });
104 });
105 });
106
107/**
108 * Build the impl Effect for a table-scoped `timestream-query` operation
109 * (`Query`, `PrepareQuery`). Timestream authorizes query actions against the
110 * tables the SQL references, so the deploy-time half grants `actions` on the
111 * bound {@link Table}'s ARN; the request passes through as-is (the SQL
112 * references the database and table by name).
113 */
114export const makeQueryTableHttpBinding = <I, A, E, R>(options: {
115 /** Fully-qualified binding tag, e.g. `AWS.Timestream.Query`. */
116 tag: string;
117 /** The distilled `timestream-query` operation. */
118 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
119 /** IAM actions granted on the table ARN. */
120 actions: readonly string[];
121}) =>
122 Effect.gen(function* () {
123 const op = yield* options.operation;
124 const describeEndpoints = yield* TSQ.describeEndpoints;
125 const withQueryEndpoint = withEndpoint(
126 discover("query", describeEndpoints({})),
127 );
128 return Effect.fn(function* (table: Table) {
129 if (!globalThis.__ALCHEMY_RUNTIME__) {
130 const host = yield* Binding.Host;
131 if (isBindingHost(host)) {
132 yield* host.bind`Allow(${host}, ${options.tag}(${table}))`({
133 policyStatements: [
134 {
135 Effect: "Allow",
136 Action: [...options.actions],
137 Resource: [Output.interpolate`${table.tableArn}`],
138 },
139 describeEndpointsStatement,
140 ],
141 });
142 }
143 }
144 return Effect.fn(`${options.tag}(${table.LogicalId})`)(function* (
145 request: I,
146 ) {
147 return yield* withQueryEndpoint(op(request));
148 });
149 });
150 });
151
152/**
153 * Build the impl Effect for an account-level `timestream-write` operation
154 * (batch-load task reads/resume — authorized against `*`, keyed by TaskId in
155 * the request). Invoked with no resource argument; the request passes
156 * through as-is.
157 */
158export const makeWriteAccountHttpBinding = <I, A, E, R>(options: {
159 /** Fully-qualified binding tag, e.g. `AWS.Timestream.DescribeBatchLoadTask`. */
160 tag: string;
161 /** The distilled `timestream-write` operation. */
162 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
163 /** IAM actions granted on `*`. */
164 actions: readonly string[];
165}) =>
166 Effect.gen(function* () {
167 const op = yield* options.operation;
168 const describeEndpoints = yield* TSW.describeEndpoints;
169 const withWriteEndpoint = withEndpoint(
170 discover("write", describeEndpoints({})),
171 );
172 return Effect.fn(function* () {
173 if (!globalThis.__ALCHEMY_RUNTIME__) {
174 const host = yield* Binding.Host;
175 if (isBindingHost(host)) {
176 yield* host.bind`Allow(${host}, ${options.tag}())`({
177 policyStatements: [
178 {
179 Effect: "Allow",
180 Action: [...options.actions],
181 Resource: ["*"],
182 },
183 describeEndpointsStatement,
184 ],
185 });
186 }
187 }
188 return Effect.fn(options.tag)(function* (request: I) {
189 return yield* withWriteEndpoint(op(request));
190 });
191 });
192 });
193
194/**
195 * Build the impl Effect for an account-level `timestream-query` operation
196 * (`CancelQuery` — authorized against `*`, keyed by QueryId in the request).
197 * Invoked with no resource argument; the request passes through as-is.
198 */
199export const makeQueryAccountHttpBinding = <I, A, E, R>(options: {
200 /** Fully-qualified binding tag, e.g. `AWS.Timestream.CancelQuery`. */
201 tag: string;
202 /** The distilled `timestream-query` operation. */
203 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
204 /** IAM actions granted on `*`. */
205 actions: readonly string[];
206}) =>
207 Effect.gen(function* () {
208 const op = yield* options.operation;
209 const describeEndpoints = yield* TSQ.describeEndpoints;
210 const withQueryEndpoint = withEndpoint(
211 discover("query", describeEndpoints({})),
212 );
213 return Effect.fn(function* () {
214 if (!globalThis.__ALCHEMY_RUNTIME__) {
215 const host = yield* Binding.Host;
216 if (isBindingHost(host)) {
217 yield* host.bind`Allow(${host}, ${options.tag}())`({
218 policyStatements: [
219 {
220 Effect: "Allow",
221 Action: [...options.actions],
222 Resource: ["*"],
223 },
224 describeEndpointsStatement,
225 ],
226 });
227 }
228 }
229 return Effect.fn(options.tag)(function* (request: I) {
230 return yield* withQueryEndpoint(op(request));
231 });
232 });
233 });