1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import type { Output as OutputType } from "../../Output.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import type { DataSet } from "./DataSet.ts";
7import type { Revision } from "./Revision.ts";
8
9/**
10 * Shared scaffolding for AWS Data Exchange HTTP bindings.
11 *
12 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
13 * a `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the three
14 * builders below. Everything except the operation, the IAM action list, and
15 * the granted ARNs is boilerplate: the runtime callable injects `DataSetId`
16 * (and `RevisionId` for revision-scoped operations) from the bound resource.
17 */
18
19/** The bound data set's own ARN (data-set-level actions). */
20export const dataSetArns = (dataSet: DataSet): OutputType<string>[] => [
21 dataSet.dataSetArn,
22];
23
24/**
25 * ARN matching every asset in the bound data set
26 * (`arn:…:data-sets/{id}/revisions/star/assets/star`) — asset-level actions
27 * like `dataexchange:SendApiAsset` authorize against the asset resource.
28 */
29export const dataSetAssetArns = (dataSet: DataSet): OutputType<string>[] => [
30 dataSet.dataSetArn.pipe(Output.map((arn) => `${arn}/revisions/*/assets/*`)),
31];
32
33/**
34 * The bound data set's ARN plus a wildcard over every sub-resource
35 * (`arn:…:data-sets/{id}` and `arn:…:data-sets/{id}/star`) — IAM wildcards
36 * match across `/`, so one pattern covers revisions and assets alike. Used
37 * by the revision/asset data-plane bindings that receive `RevisionId` and
38 * `AssetId` at runtime (e.g. a Lambda that publishes a fresh revision on a
39 * schedule needs the grant to cover revisions that do not exist yet).
40 */
41export const dataSetAndSubresourceArns = (
42 dataSet: DataSet,
43): OutputType<string>[] => [
44 dataSet.dataSetArn,
45 Output.interpolate`${dataSet.dataSetArn}/*`,
46];
47
48/** The bound revision's own ARN (revision-level actions). */
49export const revisionArns = (revision: Revision): OutputType<string>[] => [
50 revision.revisionArn,
51];
52
53/**
54 * ARN matching every asset in the bound revision
55 * (`arn:…:data-sets/{ds}/revisions/{rev}/assets/star`) — asset-level
56 * actions like `dataexchange:GetAsset` authorize against the asset resource.
57 */
58export const revisionAssetArns = (revision: Revision): OutputType<string>[] => [
59 revision.revisionArn.pipe(Output.map((arn) => `${arn}/assets/*`)),
60];
61
62/**
63 * Build the impl Effect for a Data Exchange operation scoped to a
64 * {@link DataSet}: the deploy-time half grants `actions` on `resources`
65 * (default: the data set's ARN), and the runtime half injects the data set's
66 * `DataSetId` into every request.
67 */
68export const makeDataSetHttpBinding = <
69 I extends { DataSetId: string },
70 A,
71 E,
72 R,
73>(options: {
74 /** Fully-qualified binding tag, e.g. `AWS.DataExchange.GetDataSet`. */
75 tag: string;
76 /** The distilled operation. */
77 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
78 /** IAM actions granted on `resources`. */
79 actions: readonly string[];
80 /** ARNs the actions are granted on. @default the data set ARN */
81 resources?: (dataSet: DataSet) => OutputType<string>[];
82}) =>
83 Effect.gen(function* () {
84 const op = yield* options.operation;
85
86 return Effect.fn(function* (dataSet: DataSet) {
87 const DataSetId = yield* dataSet.dataSetId;
88 if (!globalThis.__ALCHEMY_RUNTIME__) {
89 const host = yield* Binding.Host;
90 if (isBindingHost(host)) {
91 yield* host.bind`Allow(${host}, ${options.tag}(${dataSet}))`({
92 policyStatements: [
93 {
94 Effect: "Allow",
95 Action: [...options.actions],
96 Resource: (options.resources ?? dataSetArns)(dataSet),
97 },
98 ],
99 });
100 }
101 }
102 return Effect.fn(`${options.tag}(${dataSet.LogicalId})`)(function* (
103 request?: Omit<I, "DataSetId">,
104 ) {
105 const dataSetId = yield* DataSetId;
106 return yield* op({ ...request, DataSetId: dataSetId } as I);
107 });
108 });
109 });
110
111/**
112 * Build the impl Effect for a Data Exchange operation scoped to a
113 * {@link Revision}: the deploy-time half grants `actions` on `resources`
114 * (default: the revision's ARN), and the runtime half injects the revision's
115 * `DataSetId` and `RevisionId` into every request.
116 */
117export const makeRevisionHttpBinding = <
118 I extends { DataSetId: string; RevisionId: string },
119 A,
120 E,
121 R,
122>(options: {
123 /** Fully-qualified binding tag, e.g. `AWS.DataExchange.GetRevision`. */
124 tag: string;
125 /** The distilled operation. */
126 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
127 /** IAM actions granted on `resources`. */
128 actions: readonly string[];
129 /** ARNs the actions are granted on. @default the revision ARN */
130 resources?: (revision: Revision) => OutputType<string>[];
131}) =>
132 Effect.gen(function* () {
133 const op = yield* options.operation;
134
135 return Effect.fn(function* (revision: Revision) {
136 const DataSetId = yield* revision.dataSetId;
137 const RevisionId = yield* revision.revisionId;
138 if (!globalThis.__ALCHEMY_RUNTIME__) {
139 const host = yield* Binding.Host;
140 if (isBindingHost(host)) {
141 yield* host.bind`Allow(${host}, ${options.tag}(${revision}))`({
142 policyStatements: [
143 {
144 Effect: "Allow",
145 Action: [...options.actions],
146 Resource: (options.resources ?? revisionArns)(revision),
147 },
148 ],
149 });
150 }
151 }
152 return Effect.fn(`${options.tag}(${revision.LogicalId})`)(function* (
153 request?: Omit<I, "DataSetId" | "RevisionId">,
154 ) {
155 const dataSetId = yield* DataSetId;
156 const revisionId = yield* RevisionId;
157 return yield* op({
158 ...request,
159 DataSetId: dataSetId,
160 RevisionId: revisionId,
161 } as unknown as I);
162 });
163 });
164 });
165
166/**
167 * Build the impl Effect for an account-level Data Exchange operation (jobs,
168 * data grants, enumerations). The deploy-time half grants `actions` on `*`
169 * — jobs and data grants are account-scoped resources whose ids only exist
170 * at runtime, so there is no resource ARN to scope down to.
171 */
172export const makeDataExchangeAccountHttpBinding = <I, A, E, R>(options: {
173 /** Fully-qualified binding tag, e.g. `AWS.DataExchange.ListDataSets`. */
174 tag: string;
175 /** The distilled operation, invoked with the caller's request as-is. */
176 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
177 /** IAM actions granted on `*`. */
178 actions: readonly string[];
179}) =>
180 Effect.gen(function* () {
181 const op = yield* options.operation;
182
183 return Effect.fn(function* () {
184 if (!globalThis.__ALCHEMY_RUNTIME__) {
185 const host = yield* Binding.Host;
186 if (isBindingHost(host)) {
187 yield* host.bind`Allow(${host}, ${options.tag}())`({
188 policyStatements: [
189 {
190 Effect: "Allow",
191 Action: [...options.actions],
192 Resource: ["*"],
193 },
194 ],
195 });
196 }
197 }
198 return Effect.fn(options.tag)(function* (request?: I) {
199 return yield* op((request ?? {}) as I);
200 });
201 });
202 });