1import * as AppIntegrations from "@/AWS/AppIntegrations";
2import { Key } from "@/AWS/KMS";
3import * as Lambda from "@/AWS/Lambda";
4import { Bucket } from "@/AWS/S3";
5import * as Output from "@/Output";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Stream from "effect/Stream";
9import { HttpServerRequest } from "effect/http/HttpServerRequest";
10import * as HttpServerResponse from "effect/http/HttpServerResponse";
11import path from "pathe";
12
13const main = path.resolve(import.meta.dirname, "handler.ts");
14
15export class AppIntegrationsTestFunction extends Lambda.Function<Lambda.Function>()(
16 "AppIntegrationsTestFunction",
17) {}
18
19export default AppIntegrationsTestFunction.make(
20 {
21 main,
22 functionUrl: true,
23 },
24 Effect.gen(function* () {
25 const application = yield* AppIntegrations.Application("BindingsApp", {
26 namespace: "com.alchemy.bindingstest",
27 accessUrl: "https://example.com",
28 });
29
30 const bucket = yield* Bucket("BindingsContent", { forceDestroy: true });
31 const key = yield* Key("BindingsKey", {
32 description: "alchemy appintegrations bindings test key",
33 deletionWindow: "7 days",
34 });
35 const dataIntegration = yield* AppIntegrations.DataIntegration(
36 "BindingsData",
37 {
38 kmsKey: key.keyArn,
39 sourceURI: Output.interpolate`s3://${bucket.bucketName}`,
40 },
41 );
42
43 // Event source: creates the EventIntegration + the EventBridge rule on
44 // the default bus, and registers the runtime handler. Partner events
45 // never fire in tests (there is no real partner source); the deploy-time
46 // wiring is what is under test.
47 const eventIntegration = yield* AppIntegrations.consumeIntegrationEvents(
48 "BindingsPartnerEvents",
49 { source: "aws.partner/examplepartner.com/alchemy-bindings" },
50 (events) =>
51 Stream.runForEach(events, (event) =>
52 Effect.log(`integration event: ${event["detail-type"]}`),
53 ),
54 );
55
56 const createDataIntegrationAssociation =
57 yield* AppIntegrations.CreateDataIntegrationAssociation(dataIntegration);
58 const updateDataIntegrationAssociation =
59 yield* AppIntegrations.UpdateDataIntegrationAssociation(dataIntegration);
60
61 const listApplications = yield* AppIntegrations.ListApplications();
62 const listApplicationAssociations =
63 yield* AppIntegrations.ListApplicationAssociations(application);
64 const listDataIntegrations = yield* AppIntegrations.ListDataIntegrations();
65 const listDataIntegrationAssociations =
66 yield* AppIntegrations.ListDataIntegrationAssociations(dataIntegration);
67 const listEventIntegrations =
68 yield* AppIntegrations.ListEventIntegrations();
69 const listEventIntegrationAssociations =
70 yield* AppIntegrations.ListEventIntegrationAssociations(eventIntegration);
71
72 const ApplicationId = yield* application.applicationId;
73 const DataIntegrationId = yield* dataIntegration.dataIntegrationId;
74 const EventIntegrationName = yield* eventIntegration.eventIntegrationName;
75
76 return {
77 fetch: Effect.gen(function* () {
78 const request = yield* HttpServerRequest;
79 const url = new URL(request.originalUrl);
80 const pathname = url.pathname;
81
82 if (request.method === "GET" && pathname === "/applications") {
83 const result = yield* listApplications();
84 return yield* HttpServerResponse.json({
85 self: yield* ApplicationId,
86 applications: result.Applications ?? [],
87 });
88 }
89
90 if (
91 request.method === "GET" &&
92 pathname === "/application-associations"
93 ) {
94 const result = yield* listApplicationAssociations();
95 return yield* HttpServerResponse.json({
96 associations: result.ApplicationAssociations ?? [],
97 });
98 }
99
100 if (request.method === "GET" && pathname === "/data-integrations") {
101 const result = yield* listDataIntegrations();
102 return yield* HttpServerResponse.json({
103 self: yield* DataIntegrationId,
104 dataIntegrations: result.DataIntegrations ?? [],
105 });
106 }
107
108 if (
109 request.method === "POST" &&
110 pathname === "/data-integration-associations"
111 ) {
112 // Create an association (idempotently — a retried request reuses
113 // the one already created for our ClientId), then rerun its
114 // on-demand job via update. Errors are surfaced as typed tags so
115 // the test can retry through IAM propagation and report the exact
116 // failure on a terminal error.
117 const clientId = "alchemy-bindings-test-client";
118 const existing = yield* listDataIntegrationAssociations();
119 const found = (existing.DataIntegrationAssociations ?? []).find(
120 (association) => association.ClientId === clientId,
121 );
122 const created = found?.DataIntegrationAssociationArn
123 ? {
124 created: "ok" as string,
125 associationId:
126 found.DataIntegrationAssociationArn.split("/").at(-1),
127 }
128 : yield* createDataIntegrationAssociation({
129 ClientId: clientId,
130 ClientAssociationMetadata: {
131 purpose: "alchemy-bindings-test",
132 },
133 }).pipe(
134 Effect.map((result) => ({
135 created: "ok" as string,
136 associationId: result.DataIntegrationAssociationId,
137 })),
138 Effect.catch((error) =>
139 Effect.succeed({
140 created: error._tag as string,
141 message: error.message,
142 associationId: undefined as string | undefined,
143 }),
144 ),
145 );
146 const startTime = yield* Effect.sync(() => Date.now().toString());
147 const updated = created.associationId
148 ? yield* updateDataIntegrationAssociation({
149 DataIntegrationAssociationIdentifier: created.associationId,
150 ExecutionConfiguration: {
151 ExecutionMode: "ON_DEMAND",
152 OnDemandConfiguration: { StartTime: startTime },
153 },
154 }).pipe(
155 Effect.as("ok" as string),
156 Effect.catch((error) => Effect.succeed(error._tag as string)),
157 )
158 : "skipped";
159 return yield* HttpServerResponse.json({ ...created, updated });
160 }
161
162 if (
163 request.method === "GET" &&
164 pathname === "/data-integration-associations"
165 ) {
166 const result = yield* listDataIntegrationAssociations();
167 return yield* HttpServerResponse.json({
168 associations: result.DataIntegrationAssociations ?? [],
169 });
170 }
171
172 if (request.method === "GET" && pathname === "/event-integrations") {
173 const result = yield* listEventIntegrations();
174 return yield* HttpServerResponse.json({
175 self: yield* EventIntegrationName,
176 eventIntegrations: result.EventIntegrations ?? [],
177 });
178 }
179
180 if (
181 request.method === "GET" &&
182 pathname === "/event-integration-associations"
183 ) {
184 const result = yield* listEventIntegrationAssociations();
185 return yield* HttpServerResponse.json({
186 associations: result.EventIntegrationAssociations ?? [],
187 });
188 }
189
190 return yield* HttpServerResponse.json(
191 { error: "Not found", method: request.method, pathname },
192 { status: 404 },
193 );
194 }).pipe(Effect.orDie),
195 };
196 }).pipe(
197 Effect.provide(
198 Layer.mergeAll(
199 Lambda.EventSource,
200 AppIntegrations.CreateDataIntegrationAssociationHttp,
201 AppIntegrations.UpdateDataIntegrationAssociationHttp,
202 AppIntegrations.ListApplicationsHttp,
203 AppIntegrations.ListApplicationAssociationsHttp,
204 AppIntegrations.ListDataIntegrationsHttp,
205 AppIntegrations.ListDataIntegrationAssociationsHttp,
206 AppIntegrations.ListEventIntegrationsHttp,
207 AppIntegrations.ListEventIntegrationAssociationsHttp,
208 ),
209 ),
210 ),
211);