1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import type * as Output from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5
6/**
7 * Options for {@link makeAppFlowHttpBinding}. Everything except these five
8 * inputs is identical across AppFlow's per-operation HTTP bindings.
9 */
10export interface AppFlowHttpBindingOptions<
11 R extends { readonly LogicalId: string },
12 K extends string,
13 I extends { [P in K]?: string },
14 A,
15 E,
16 OpR,
17> {
18 /**
19 * The AppFlow action name, e.g. `"StartFlow"`. Becomes the IAM action
20 * (`appflow:StartFlow`), the bind label, and the runtime span name.
21 */
22 action: string;
23 /** The distilled AppFlow operation invoked at runtime. */
24 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, OpR>;
25 /**
26 * Resolve the identifier the operation is keyed by (the flow name or the
27 * connector profile name).
28 */
29 identifier: (resource: R) => Output.Output<string, never>;
30 /** The request field the resolved identifier is injected as. */
31 requestKey: K;
32 /** The IAM policy resources the action is granted on. */
33 resources: (resource: R) => Array<string | Output.Output<string, never>>;
34}
35
36/**
37 * Shared scaffolding for AppFlow per-operation HTTP bindings.
38 *
39 * Every AppFlow runtime binding follows the same recipe: resolve the
40 * identifier (flow name / connector profile name), register the IAM grant on
41 * the binding host at deploy time, then invoke the distilled operation with
42 * the identifier injected. This factory owns that recipe so each `{Op}Http.ts`
43 * is a thin `Layer.effect(Cap, makeAppFlowHttpBinding({ ... }))` call whose
44 * request/response/error types are still checked against the capability
45 * contract at the `Layer.effect` site.
46 *
47 * Internal scaffolding — NOT exported from `index.ts`.
48 */
49export const makeAppFlowHttpBinding = <
50 R extends { readonly LogicalId: string },
51 K extends string,
52 I extends { [P in K]?: string },
53 A,
54 E,
55 OpR,
56>(
57 options: AppFlowHttpBindingOptions<R, K, I, A, E, OpR>,
58) =>
59 Effect.gen(function* () {
60 const operation = yield* options.operation;
61
62 return Effect.fn(function* (resource: R) {
63 const Identifier = yield* options.identifier(resource);
64 if (!globalThis.__ALCHEMY_RUNTIME__) {
65 const host = yield* Binding.Host;
66 if (isBindingHost(host)) {
67 yield* host.bind`Allow(${host}, AWS.AppFlow.${options.action}(${resource}))`(
68 {
69 policyStatements: [
70 {
71 Effect: "Allow",
72 Action: [`appflow:${options.action}`],
73 Resource: options.resources(resource),
74 },
75 ],
76 },
77 );
78 }
79 }
80 return Effect.fn(`AWS.AppFlow.${options.action}(${resource.LogicalId})`)(
81 function* (request?: Omit<I, K>) {
82 // The compiler cannot relate `Omit<I, K>` plus a computed generic
83 // key back to `I`, so the identifier injection carries one
84 // contained cast. Concrete request/response types are still fully
85 // checked where each `Layer.effect(Cap, ...)` matches this factory's
86 // inferred shape against the capability contract.
87 return yield* operation({
88 ...request,
89 [options.requestKey]: yield* Identifier,
90 } as unknown as I);
91 },
92 );
93 });
94 });