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 { DataSource } from "./DataSource.ts";
6import type { Index } from "./SearchIndex.ts";
7
8/**
9 * Shared scaffolding for AWS Kendra HTTP bindings.
10 *
11 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
12 * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the two
13 * builders below. Everything except the operation and the IAM action list is
14 * boilerplate: every Kendra data-plane operation is scoped to one index
15 * (whose id is injected as `IndexId` and whose ARN receives the grant), and
16 * the sync-job operations are additionally scoped to one data source (whose
17 * id is injected as `Id` and whose ARN receives the grant alongside its
18 * parent index's).
19 */
20
21/**
22 * Build the impl Effect for an index-scoped Kendra operation (query,
23 * retrieve, document batches, principal mapping, suggestions config, …): the
24 * runtime callable injects the bound {@link Index}'s id as `IndexId` and the
25 * deploy-time half grants `actions` on the index ARN (plus any
26 * `subResources` suffix patterns, e.g. `data-source/*` for the
27 * principal-mapping operations that also act on data-source-scoped groups).
28 *
29 * `prepare` (optional) maps a friendlier public request shape onto the wire
30 * request — e.g. `UpdateQuerySuggestionsConfig` converts a `Duration.Input`
31 * into the wire `QueryLogLookBackWindowInDays`. It defaults to identity.
32 */
33export const makeKendraIndexHttpBinding = <
34 I extends { IndexId: string },
35 A,
36 E,
37 R,
38 Req = Omit<I, "IndexId">,
39>(options: {
40 /** Fully-qualified binding tag, e.g. `AWS.Kendra.Query`. */
41 tag: string;
42 /** The distilled operation; `IndexId` is injected from the index. */
43 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
44 /** IAM actions granted on the index ARN. */
45 actions: readonly string[];
46 /**
47 * Extra ARN suffix patterns (relative to the index ARN) the actions are
48 * also granted on, e.g. `data-source/*` or
49 * `access-control-configuration/*`.
50 */
51 subResources?: readonly string[];
52 /** Map the public request shape to the wire request (defaults to identity). */
53 prepare?: (request: Req) => Omit<I, "IndexId">;
54}) =>
55 Effect.gen(function* () {
56 const op = yield* options.operation;
57
58 return Effect.fn(function* (index: Index) {
59 const IndexId = yield* index.id;
60 if (!globalThis.__ALCHEMY_RUNTIME__) {
61 const host = yield* Binding.Host;
62 if (isBindingHost(host)) {
63 yield* host.bind`Allow(${host}, ${options.tag}(${index}))`({
64 policyStatements: [
65 {
66 Effect: "Allow",
67 Action: [...options.actions],
68 Resource: [
69 index.arn,
70 ...(options.subResources ?? []).map(
71 (suffix) => Output.interpolate`${index.arn}/${suffix}`,
72 ),
73 ],
74 },
75 ],
76 });
77 }
78 }
79 return Effect.fn(`${options.tag}(${index.LogicalId})`)(function* (
80 request?: Req,
81 ) {
82 const wire = options.prepare
83 ? options.prepare(request as Req)
84 : (request as unknown as Omit<I, "IndexId"> | undefined);
85 return yield* op({
86 ...wire,
87 IndexId: yield* IndexId,
88 } as I);
89 });
90 });
91 });
92
93/**
94 * Build the impl Effect for a data-source-scoped Kendra operation (the sync
95 * job start/stop/list trio): the runtime callable injects the bound
96 * {@link DataSource}'s id as `Id` and its parent index's id as `IndexId`;
97 * the deploy-time half grants `actions` on the data source ARN **and** the
98 * parent index ARN — Kendra authorizes sync-job actions against both.
99 */
100export const makeKendraDataSourceHttpBinding = <
101 I extends { IndexId: string; Id: string },
102 A,
103 E,
104 R,
105>(options: {
106 /** Fully-qualified binding tag, e.g. `AWS.Kendra.StartDataSourceSyncJob`. */
107 tag: string;
108 /**
109 * The distilled operation; `Id` and `IndexId` are injected from the data
110 * source.
111 */
112 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
113 /** IAM actions granted on the data source ARN + its parent index ARN. */
114 actions: readonly string[];
115}) =>
116 Effect.gen(function* () {
117 const op = yield* options.operation;
118
119 return Effect.fn(function* (dataSource: DataSource) {
120 const IndexId = yield* dataSource.indexId;
121 const Id = yield* dataSource.id;
122 if (!globalThis.__ALCHEMY_RUNTIME__) {
123 const host = yield* Binding.Host;
124 if (isBindingHost(host)) {
125 yield* host.bind`Allow(${host}, ${options.tag}(${dataSource}))`({
126 policyStatements: [
127 {
128 Effect: "Allow",
129 Action: [...options.actions],
130 Resource: [
131 dataSource.arn,
132 // The data source ARN is
133 // `…:index/{indexId}/data-source/{id}` — the parent index
134 // ARN is its prefix.
135 dataSource.arn.pipe(
136 Output.map((arn) => arn.split("/data-source/")[0]!),
137 ),
138 ],
139 },
140 ],
141 });
142 }
143 }
144 return Effect.fn(`${options.tag}(${dataSource.LogicalId})`)(function* (
145 request?: Omit<I, "IndexId" | "Id">,
146 ) {
147 return yield* op({
148 ...request,
149 IndexId: yield* IndexId,
150 Id: yield* Id,
151 } as I);
152 });
153 });
154 });