1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import type { Role } from "../IAM/Role.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import type { FHIRDatastore } from "./FHIRDatastore.ts";
7
8/**
9 * Shared scaffolding for AWS HealthLake 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 builders
13 * below. Everything except the operation and the IAM action list is
14 * boilerplate: every HealthLake job operation is scoped to one FHIR data
15 * store, whose id is injected as `DatastoreId` and whose ARN receives the
16 * grant. The `Start*Job` operations additionally inject the bound
17 * data-access role and a scoped `iam:PassRole` grant.
18 */
19
20/**
21 * Build the impl Effect for a data-store-scoped HealthLake operation
22 * (describe/list import and export jobs): the runtime callable injects the
23 * bound {@link FHIRDatastore}'s id as `DatastoreId` and the deploy-time half
24 * grants `actions` on the data store ARN.
25 */
26export const makeHealthLakeDatastoreHttpBinding = <
27 I extends { DatastoreId: string },
28 A,
29 E,
30 R,
31>(options: {
32 /** Fully-qualified binding tag, e.g. `AWS.HealthLake.DescribeFHIRImportJob`. */
33 tag: string;
34 /** The distilled operation; `DatastoreId` is injected from the data store. */
35 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
36 /** IAM actions granted on the data store ARN. */
37 actions: readonly string[];
38}) =>
39 Effect.gen(function* () {
40 const op = yield* options.operation;
41
42 return Effect.fn(function* (datastore: FHIRDatastore) {
43 const DatastoreId = yield* datastore.datastoreId;
44 if (!globalThis.__ALCHEMY_RUNTIME__) {
45 const host = yield* Binding.Host;
46 if (isBindingHost(host)) {
47 yield* host.bind`Allow(${host}, ${options.tag}(${datastore}))`({
48 policyStatements: [
49 {
50 Effect: "Allow",
51 Action: [...options.actions],
52 Resource: [Output.interpolate`${datastore.datastoreArn}`],
53 },
54 ],
55 });
56 }
57 }
58 return Effect.fn(`${options.tag}(${datastore.LogicalId})`)(function* (
59 request?: Omit<I, "DatastoreId">,
60 ) {
61 return yield* op({
62 ...request,
63 DatastoreId: yield* DatastoreId,
64 } as I);
65 });
66 });
67 });
68
69/**
70 * Build the impl Effect for a HealthLake `Start*Job` operation: the binding
71 * is constructed with the {@link FHIRDatastore} **and the data-access role**
72 * (the IAM role HealthLake assumes to read/write the S3 locations; its trust
73 * policy must allow `healthlake.amazonaws.com`). The runtime callable injects
74 * the data store id as `DatastoreId` and the role's ARN as
75 * `DataAccessRoleArn`; the deploy-time half grants `actions` on the data
76 * store ARN plus `iam:PassRole` on the role — without the PassRole grant,
77 * `Start*Job` fails only at runtime with an AccessDeniedException.
78 */
79export const makeHealthLakeStartJobHttpBinding = <
80 I extends { DatastoreId: string; DataAccessRoleArn: string },
81 A,
82 E,
83 R,
84>(options: {
85 /** Fully-qualified binding tag, e.g. `AWS.HealthLake.StartFHIRExportJob`. */
86 tag: string;
87 /**
88 * The distilled operation; `DatastoreId` and `DataAccessRoleArn` are
89 * injected from the bound resources.
90 */
91 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
92 /** IAM actions granted on the data store ARN. */
93 actions: readonly string[];
94}) =>
95 Effect.gen(function* () {
96 const op = yield* options.operation;
97
98 return Effect.fn(function* (
99 datastore: FHIRDatastore,
100 dataAccessRole: Role,
101 ) {
102 const DatastoreId = yield* datastore.datastoreId;
103 const RoleArn = yield* dataAccessRole.roleArn;
104 if (!globalThis.__ALCHEMY_RUNTIME__) {
105 const host = yield* Binding.Host;
106 if (isBindingHost(host)) {
107 yield* host.bind`Allow(${host}, ${options.tag}(${datastore}, ${dataAccessRole}))`(
108 {
109 policyStatements: [
110 {
111 Effect: "Allow",
112 Action: [...options.actions],
113 Resource: [Output.interpolate`${datastore.datastoreArn}`],
114 },
115 // CRITICAL: without iam:PassRole on the data-access role,
116 // Start*Job fails only at runtime with an AccessDenied.
117 {
118 Effect: "Allow",
119 Action: ["iam:PassRole"],
120 Resource: [Output.interpolate`${dataAccessRole.roleArn}`],
121 Condition: {
122 StringEquals: {
123 "iam:PassedToService": "healthlake.amazonaws.com",
124 },
125 },
126 },
127 ],
128 },
129 );
130 }
131 }
132 return Effect.fn(`${options.tag}(${datastore.LogicalId})`)(function* (
133 request: Omit<I, "DatastoreId" | "DataAccessRoleArn"> & {
134 DataAccessRoleArn?: string;
135 },
136 ) {
137 return yield* op({
138 ...request,
139 DatastoreId: yield* DatastoreId,
140 DataAccessRoleArn: request.DataAccessRoleArn ?? (yield* RoleArn),
141 } as I);
142 });
143 });
144 });