1import * as DataZone from "@/AWS/DataZone";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Result from "effect/Result";
7import * as Stream from "effect/Stream";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "handler.ts");
13
14/**
15 * Deterministic domain name so the test can find the domain id out-of-band
16 * (needed to create a DataZone user profile for the function's role).
17 */
18export const BINDINGS_DOMAIN_NAME = "alchemy-datazone-bindings-test";
19
20/**
21 * Deterministic project name so the test can find the project id
22 * out-of-band (needed to add the function's user profile as a member —
23 * inventory search is project-scoped).
24 */
25export const BINDINGS_PROJECT_NAME = "alchemy-datazone-bindings-project";
26
27export class DataZoneTestFunction extends Lambda.Function<Lambda.Function>()(
28 "DataZoneTestFunction",
29) {}
30
31export default DataZoneTestFunction.make(
32 {
33 main,
34 functionUrl: true,
35 timeout: Duration.seconds(30),
36 },
37 Effect.gen(function* () {
38 // The domain every domain-scoped binding is bound to.
39 const domain = yield* DataZone.Domain("BindingsDomain", {
40 name: BINDINGS_DOMAIN_NAME,
41 description: "alchemy datazone bindings fixture domain",
42 });
43
44 // A project for the project-scoped calls (inventory search requires an
45 // owning project; subscription listing requires a project filter).
46 const project = yield* DataZone.Project("BindingsProject", {
47 domainId: domain.domainId,
48 name: BINDINGS_PROJECT_NAME,
49 description: "alchemy datazone bindings fixture project",
50 });
51 const ProjectId = yield* project.projectId;
52
53 // Event source: subscribe the host to DataZone workflow events. The
54 // deploy proves the EventBridge rule + invoke permission wiring.
55 yield* DataZone.consumeDataZoneEvents(
56 { detailTypes: ["Subscription Request Created"] },
57 (events) =>
58 Stream.runForEach(events, (event) =>
59 Effect.log(`datazone event: ${event.detail.metadata?.id}`),
60 ),
61 );
62
63 const bound = {
64 search: yield* DataZone.Search(domain),
65 searchListings: yield* DataZone.SearchListings(domain),
66 searchTypes: yield* DataZone.SearchTypes(domain),
67 getListing: yield* DataZone.GetListing(domain),
68 getAsset: yield* DataZone.GetAsset(domain),
69 createAsset: yield* DataZone.CreateAsset(domain),
70 createAssetRevision: yield* DataZone.CreateAssetRevision(domain),
71 acceptPredictions: yield* DataZone.AcceptPredictions(domain),
72 rejectPredictions: yield* DataZone.RejectPredictions(domain),
73 postLineageEvent: yield* DataZone.PostLineageEvent(domain),
74 getLineageNode: yield* DataZone.GetLineageNode(domain),
75 postTimeSeriesDataPoints:
76 yield* DataZone.PostTimeSeriesDataPoints(domain),
77 listTimeSeriesDataPoints:
78 yield* DataZone.ListTimeSeriesDataPoints(domain),
79 getTimeSeriesDataPoint: yield* DataZone.GetTimeSeriesDataPoint(domain),
80 createSubscriptionRequest:
81 yield* DataZone.CreateSubscriptionRequest(domain),
82 acceptSubscriptionRequest:
83 yield* DataZone.AcceptSubscriptionRequest(domain),
84 rejectSubscriptionRequest:
85 yield* DataZone.RejectSubscriptionRequest(domain),
86 getSubscription: yield* DataZone.GetSubscription(domain),
87 listSubscriptions: yield* DataZone.ListSubscriptions(domain),
88 listSubscriptionRequests:
89 yield* DataZone.ListSubscriptionRequests(domain),
90 cancelSubscription: yield* DataZone.CancelSubscription(domain),
91 revokeSubscription: yield* DataZone.RevokeSubscription(domain),
92 startDataSourceRun: yield* DataZone.StartDataSourceRun(domain),
93 getDataSourceRun: yield* DataZone.GetDataSourceRun(domain),
94 listDataSourceRuns: yield* DataZone.ListDataSourceRuns(domain),
95 startMetadataGenerationRun:
96 yield* DataZone.StartMetadataGenerationRun(domain),
97 getMetadataGenerationRun:
98 yield* DataZone.GetMetadataGenerationRun(domain),
99 getIamPortalLoginUrl: yield* DataZone.GetIamPortalLoginUrl(domain),
100 listNotifications: yield* DataZone.ListNotifications(domain),
101 getUserProfile: yield* DataZone.GetUserProfile(domain),
102 };
103
104 /**
105 * Run a DataZone call and answer `{ ok: true, ... }` on success, or a
106 * 502 carrying the typed error tag on failure — so the test's retry
107 * loop surfaces the exact DataZone error instead of an opaque 500.
108 */
109 const respond = <A, E>(
110 effect: Effect.Effect<A, E>,
111 body: (value: A) => Record<string, unknown>,
112 ) =>
113 Effect.gen(function* () {
114 const result = yield* Effect.result(effect);
115 return yield* Result.isSuccess(result)
116 ? HttpServerResponse.json({ ok: true, ...body(result.success) })
117 : HttpServerResponse.json(
118 { ok: false, error: String(result.failure) },
119 { status: 502 },
120 );
121 });
122
123 return {
124 fetch: Effect.gen(function* () {
125 const request = yield* HttpServerRequest;
126 const url = new URL(request.originalUrl);
127 const pathname = url.pathname;
128
129 if (request.method === "GET" && pathname === "/bindings") {
130 return yield* HttpServerResponse.json({
131 bound: Object.keys(bound),
132 });
133 }
134
135 // Inventory search: the domain id is injected from the binding;
136 // inventory assets are project-scoped, so the owning project is
137 // required.
138 if (request.method === "GET" && pathname === "/search") {
139 const owningProjectIdentifier = yield* ProjectId;
140 return yield* respond(
141 bound.search({ searchScope: "ASSET", owningProjectIdentifier }),
142 (result) => ({
143 totalMatchCount: result.totalMatchCount ?? 0,
144 items: (result.items ?? []).length,
145 }),
146 );
147 }
148
149 // Listing search over the published catalog.
150 if (request.method === "GET" && pathname === "/listings") {
151 return yield* respond(bound.searchListings({}), (result) => ({
152 items: (result.items ?? []).length,
153 }));
154 }
155
156 // Approved subscriptions (empty in a fresh domain). DataZone
157 // requires a project (or listing/principal) filter.
158 if (request.method === "GET" && pathname === "/subscriptions") {
159 const owningProjectId = yield* ProjectId;
160 return yield* respond(
161 bound.listSubscriptions({ status: "APPROVED", owningProjectId }),
162 (result) => ({ items: (result.items ?? []).length }),
163 );
164 }
165
166 // Pending subscription requests (empty in a fresh domain). Same
167 // project filter requirement as /subscriptions.
168 if (request.method === "GET" && pathname === "/requests") {
169 const owningProjectId = yield* ProjectId;
170 return yield* respond(
171 bound.listSubscriptionRequests({
172 status: "PENDING",
173 owningProjectId,
174 }),
175 (result) => ({ items: (result.items ?? []).length }),
176 );
177 }
178
179 // Single-use data portal sign-in URL.
180 if (request.method === "GET" && pathname === "/portal") {
181 return yield* respond(bound.getIamPortalLoginUrl(), (result) => ({
182 authCodeUrl: result.authCodeUrl,
183 userProfileId: result.userProfileId,
184 }));
185 }
186
187 return yield* HttpServerResponse.json(
188 { error: "Not found", method: request.method, pathname },
189 { status: 404 },
190 );
191 }).pipe(Effect.orDie),
192 };
193 }).pipe(
194 Effect.provide(
195 Layer.mergeAll(
196 Lambda.EventSource,
197 DataZone.SearchHttp,
198 DataZone.SearchListingsHttp,
199 DataZone.SearchTypesHttp,
200 DataZone.GetListingHttp,
201 DataZone.GetAssetHttp,
202 DataZone.CreateAssetHttp,
203 DataZone.CreateAssetRevisionHttp,
204 DataZone.AcceptPredictionsHttp,
205 DataZone.RejectPredictionsHttp,
206 DataZone.PostLineageEventHttp,
207 DataZone.GetLineageNodeHttp,
208 DataZone.PostTimeSeriesDataPointsHttp,
209 DataZone.ListTimeSeriesDataPointsHttp,
210 DataZone.GetTimeSeriesDataPointHttp,
211 DataZone.CreateSubscriptionRequestHttp,
212 DataZone.AcceptSubscriptionRequestHttp,
213 DataZone.RejectSubscriptionRequestHttp,
214 DataZone.GetSubscriptionHttp,
215 DataZone.ListSubscriptionsHttp,
216 DataZone.ListSubscriptionRequestsHttp,
217 DataZone.CancelSubscriptionHttp,
218 DataZone.RevokeSubscriptionHttp,
219 DataZone.StartDataSourceRunHttp,
220 DataZone.GetDataSourceRunHttp,
221 DataZone.ListDataSourceRunsHttp,
222 DataZone.StartMetadataGenerationRunHttp,
223 DataZone.GetMetadataGenerationRunHttp,
224 DataZone.GetIamPortalLoginUrlHttp,
225 DataZone.ListNotificationsHttp,
226 DataZone.GetUserProfileHttp,
227 ),
228 ),
229 ),
230);