1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Stream from "effect/Stream";
4
5import * as Namespace from "../../Namespace.ts";
6import { sanitizeRuleName } from "../IoT/internal.ts";
7import { TopicRule } from "../IoT/TopicRule.ts";
8import {
9 type IoTMessage,
10 TopicRuleEventSource as IoTTopicRuleEventSource,
11 type TopicRuleEventSourceProps,
12 type TopicRuleEventSourceService,
13} from "../IoT/TopicRuleEventSource.ts";
14import * as Lambda from "./Function.ts";
15import { Permission as LambdaPermission } from "./Permission.ts";
16
17/**
18 * An IoT rule action delivers the raw SQL `SELECT` payload as the Lambda
19 * event — an arbitrary JSON object. We discriminate it from the Records-based
20 * AWS events (SQS/S3/SNS/DynamoDB) and from HTTP invocations (function URL /
21 * API Gateway / ALB, which carry `requestContext`) so a single function can
22 * host an IoT source alongside its HTTP handler.
23 */
24export const isIoTMessage = (event: any): event is IoTMessage =>
25 event != null &&
26 typeof event === "object" &&
27 !Array.isArray(event) &&
28 !("Records" in event) &&
29 !("requestContext" in event);
30
31/** @binding */
32export const TopicRuleEventSource = Layer.effect(
33 IoTTopicRuleEventSource,
34 // @effect-diagnostics-next-line missingEffectContext:off
35 Effect.gen(function* () {
36 const host = yield* Lambda.Function;
37 const Rule = yield* TopicRule;
38 const Permission = yield* LambdaPermission;
39
40 return Effect.fn(function* <Req = never>(
41 topicFilter: string,
42 props: TopicRuleEventSourceProps,
43 process: (
44 stream: Stream.Stream<IoTMessage>,
45 ) => Effect.Effect<void, never, Req>,
46 ) {
47 // Deploy-time: create the IoT topic rule with a Lambda action targeting
48 // this function, and grant iot.amazonaws.com permission to invoke it
49 // (scoped to the rule ARN). Skipped once running inside the deployed
50 // function, where the only work is registering the runtime handler.
51 if (!globalThis.__ALCHEMY_RUNTIME__) {
52 yield* Namespace.push(
53 host.LogicalId,
54 Effect.gen(function* () {
55 const ruleName =
56 props.ruleName ??
57 sanitizeRuleName(`${host.LogicalId}_${topicFilter}`);
58 const rule = yield* Rule(`${host.LogicalId}-TopicRule`, {
59 ruleName,
60 sql: props.sql ?? `SELECT * FROM '${topicFilter}'`,
61 awsIotSqlVersion: props.awsIotSqlVersion,
62 actions: [{ lambda: { functionArn: host.functionArn } }],
63 });
64
65 yield* Permission(`AWS.IoT.TopicRuleInvoke(${ruleName})`, {
66 action: "lambda:InvokeFunction",
67 functionName: host.functionName,
68 principal: "iot.amazonaws.com",
69 sourceArn: rule.ruleArn,
70 });
71 }),
72 );
73 }
74
75 yield* host.listen(
76 Effect.gen(function* () {
77 return (event: any) => {
78 if (isIoTMessage(event)) {
79 return process(Stream.fromArray([event])).pipe(Effect.orDie);
80 }
81 };
82 }),
83 );
84 }) as TopicRuleEventSourceService;
85 }),
86);