1import * as Effect from "effect/Effect";
2import { createHash } from "node:crypto";
3import type { Function as LambdaFunction } from "../Lambda/Function.ts";
4import { Permission as LambdaPermission } from "../Lambda/Permission.ts";
5import type { EventBus } from "./EventBus.ts";
6import { Rule, type RuleProps, type RuleTarget } from "./Rule.ts";
7
8interface EventDescriptor {
9 id?: string;
10 bus?: EventBus;
11 pattern: Record<string, any>;
12 props?: Pick<RuleProps, "description" | "state">;
13}
14
15export interface LambdaRouteTargetProps extends Pick<
16 RuleTarget,
17 | "Input"
18 | "InputPath"
19 | "InputTransformer"
20 | "RetryPolicy"
21 | "DeadLetterConfig"
22> {}
23
24/**
25 * Routes matching events from an EventBridge bus to a Lambda function.
26 *
27 * Creates a {@link Rule} targeting the function and a Lambda permission
28 * allowing `events.amazonaws.com` to invoke it. Usually reached through the
29 * `events(...)` builder rather than called directly.
30 * **Example:** Route Matching Events to a Lambda Function
31 * ```typescript
32 * yield* AWS.EventBridge.events(bus, { source: ["my.app"] }).toLambda(fn);
33 * ```
34 *
35 * **Example:** Transform the Event Payload Before Invoking
36 * ```typescript
37 * yield* AWS.EventBridge.events(bus, { source: ["my.app"] }).toLambda(fn, {
38 * InputTransformer: {
39 * InputPathsMap: { orderId: "$.detail.orderId" },
40 * InputTemplate: '{"orderId": <orderId>}',
41 * },
42 * });
43 * ```
44 *
45 * @binding
46 */
47export const toLambda = (
48 descriptor: EventDescriptor,
49 fn: LambdaFunction,
50 props: LambdaRouteTargetProps = {},
51) =>
52 Effect.gen(function* () {
53 const routeId =
54 descriptor.id ?? createRouteId(descriptor, `${fn.LogicalId}Lambda`);
55
56 const rule = yield* Rule(routeId, {
57 description: descriptor.props?.description,
58 state: descriptor.props?.state,
59 eventBusName: descriptor.bus?.eventBusName,
60 eventPattern: descriptor.pattern,
61 targets: [
62 {
63 Id: `${fn.LogicalId}Target`,
64 Arn: fn.functionArn as any,
65 Input: props.Input,
66 InputPath: props.InputPath,
67 InputTransformer: props.InputTransformer,
68 RetryPolicy: props.RetryPolicy,
69 DeadLetterConfig: props.DeadLetterConfig,
70 },
71 ],
72 });
73
74 // Deploy-time: grant EventBridge permission to invoke the Lambda. Skipped
75 // once running inside the deployed Function by the global guard. The
76 // permission is explicitly named, so no Namespace.push is required.
77 if (!globalThis.__ALCHEMY_RUNTIME__) {
78 yield* LambdaPermission(`${routeId}${fn.LogicalId}InvokePermission`, {
79 action: "lambda:InvokeFunction",
80 functionName: fn.functionName,
81 principal: "events.amazonaws.com",
82 sourceArn: rule.ruleArn as any,
83 }).pipe(Effect.asVoid);
84 }
85
86 return rule;
87 });
88
89const createRouteId = (descriptor: EventDescriptor, suffix: string) =>
90 `EventBridge${createHash("sha1")
91 .update(
92 JSON.stringify({
93 bus: descriptor.bus?.LogicalId ?? "default",
94 pattern: descriptor.pattern,
95 suffix,
96 }),
97 )
98 .digest("hex")
99 .slice(0, 10)}`;