1import * as IoTSiteWise from "@/AWS/IoTSiteWise";
2import * as Lambda from "@/AWS/Lambda";
3import * as Cause from "effect/Cause";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "handler.ts");
12
13export class IoTSiteWiseTestFunction extends Lambda.Function<Lambda.Function>()(
14 "IoTSiteWiseTestFunction",
15) {}
16
17export default IoTSiteWiseTestFunction.make(
18 {
19 main,
20 functionUrl: true,
21 timeout: Duration.seconds(30),
22 },
23 Effect.gen(function* () {
24 const model = yield* IoTSiteWise.AssetModel("BindingsPumpModel", {
25 assetModelDescription: "alchemy iotsitewise bindings fixture",
26 assetModelProperties: [
27 {
28 name: "Temperature",
29 dataType: "DOUBLE",
30 unit: "Celsius",
31 type: { measurement: {} },
32 },
33 ],
34 tags: { fixture: "iotsitewise-bindings" },
35 });
36 const asset = yield* IoTSiteWise.Asset("BindingsPump", {
37 assetModelId: model.assetModelId,
38 assetDescription: "alchemy iotsitewise bindings fixture asset",
39 tags: { fixture: "iotsitewise-bindings" },
40 });
41
42 const describeAsset = yield* IoTSiteWise.DescribeAsset(asset);
43 const listProperties = yield* IoTSiteWise.ListAssetProperties(asset);
44 const putValues = yield* IoTSiteWise.BatchPutAssetPropertyValue(asset);
45 const getValue = yield* IoTSiteWise.GetAssetPropertyValue(asset);
46 const getHistory = yield* IoTSiteWise.GetAssetPropertyValueHistory(asset);
47 const getAggregates = yield* IoTSiteWise.GetAssetPropertyAggregates(asset);
48 const getInterpolated =
49 yield* IoTSiteWise.GetInterpolatedAssetPropertyValues(asset);
50 const executeQuery = yield* IoTSiteWise.ExecuteQuery();
51
52 // Resolve the service-assigned id of the Temperature property by name —
53 // exercises the DescribeAsset binding on every data-plane route.
54 const temperaturePropertyId = Effect.gen(function* () {
55 const described = yield* describeAsset();
56 const property = described.assetProperties.find(
57 (p) => p.name === "Temperature",
58 );
59 if (!property) {
60 return yield* Effect.fail(
61 new Error("Temperature property not found on the bound asset"),
62 );
63 }
64 return property.id;
65 });
66
67 return {
68 fetch: Effect.gen(function* () {
69 const request = yield* HttpServerRequest;
70 const pathname = new URL(request.originalUrl).pathname;
71
72 // Cheap readiness route — no AWS call.
73 if (request.method === "GET" && pathname === "/ping") {
74 return yield* HttpServerResponse.json({ ok: true });
75 }
76
77 if (request.method === "POST" && pathname === "/describe") {
78 const described = yield* describeAsset();
79 return yield* HttpServerResponse.json({
80 assetId: described.assetId,
81 assetName: described.assetName,
82 propertyNames: described.assetProperties.map((p) => p.name),
83 });
84 }
85
86 if (request.method === "POST" && pathname === "/properties") {
87 const listed = yield* listProperties({ filter: "ALL" });
88 return yield* HttpServerResponse.json({
89 count: listed.assetPropertySummaries.length,
90 });
91 }
92
93 if (request.method === "POST" && pathname === "/put") {
94 const propertyId = yield* temperaturePropertyId;
95 // Fresh timestamp per call so retried requests never collide with
96 // an already-ingested TQV.
97 const now = yield* Effect.sync(() => Math.floor(Date.now() / 1000));
98 const result = yield* putValues({
99 entries: [
100 {
101 entryId: `temp-${now}`,
102 propertyId,
103 propertyValues: [
104 {
105 value: { doubleValue: 23.5 },
106 timestamp: { timeInSeconds: now },
107 quality: "GOOD",
108 },
109 ],
110 },
111 ],
112 });
113 return yield* HttpServerResponse.json({
114 errorCount: result.errorEntries.length,
115 errorCodes: result.errorEntries.flatMap((entry) =>
116 entry.errors.map((error) => error.errorCode),
117 ),
118 });
119 }
120
121 if (request.method === "POST" && pathname === "/value") {
122 const propertyId = yield* temperaturePropertyId;
123 const result = yield* getValue({ propertyId });
124 return yield* HttpServerResponse.json({
125 doubleValue: result.propertyValue?.value.doubleValue ?? null,
126 timeInSeconds:
127 result.propertyValue?.timestamp.timeInSeconds ?? null,
128 });
129 }
130
131 if (request.method === "POST" && pathname === "/history") {
132 const propertyId = yield* temperaturePropertyId;
133 const now = yield* Effect.sync(() => Date.now());
134 const result = yield* getHistory({
135 propertyId,
136 startDate: new Date(now - 600_000),
137 endDate: new Date(now),
138 timeOrdering: "DESCENDING",
139 });
140 return yield* HttpServerResponse.json({
141 count: result.assetPropertyValueHistory.length,
142 });
143 }
144
145 if (request.method === "POST" && pathname === "/aggregates") {
146 const propertyId = yield* temperaturePropertyId;
147 const now = yield* Effect.sync(() => Date.now());
148 const result = yield* getAggregates({
149 propertyId,
150 aggregateTypes: ["AVERAGE"],
151 resolution: "1m",
152 startDate: new Date(now - 3_600_000),
153 endDate: new Date(now),
154 });
155 return yield* HttpServerResponse.json({
156 ok: true,
157 count: result.aggregatedValues.length,
158 });
159 }
160
161 if (request.method === "POST" && pathname === "/interpolated") {
162 const propertyId = yield* temperaturePropertyId;
163 const now = yield* Effect.sync(() => Math.floor(Date.now() / 1000));
164 const result = yield* getInterpolated({
165 propertyId,
166 startTimeInSeconds: now - 3600,
167 endTimeInSeconds: now,
168 intervalInSeconds: 60,
169 quality: "GOOD",
170 type: "LINEAR_INTERPOLATION",
171 });
172 return yield* HttpServerResponse.json({
173 ok: true,
174 count: result.interpolatedAssetPropertyValues.length,
175 });
176 }
177
178 if (request.method === "POST" && pathname === "/query") {
179 const described = yield* describeAsset();
180 const result = yield* executeQuery({
181 queryStatement: `SELECT asset_id, asset_name FROM asset WHERE asset_id = '${described.assetId}'`,
182 });
183 return yield* HttpServerResponse.json({
184 ok: true,
185 rowCount: result.rows?.length ?? 0,
186 });
187 }
188
189 return yield* HttpServerResponse.json(
190 { error: "Not found", method: request.method, pathname },
191 { status: 404 },
192 );
193 }).pipe(
194 // Surface the failure cause in the 500 body so the test's retry
195 // error carries the real diagnostic instead of an opaque
196 // "Internal Server Error".
197 Effect.catchCause((cause) =>
198 HttpServerResponse.json(
199 { error: Cause.pretty(cause) },
200 { status: 500 },
201 ),
202 ),
203 ),
204 };
205 }).pipe(
206 Effect.provide(
207 Layer.mergeAll(
208 IoTSiteWise.DescribeAssetHttp,
209 IoTSiteWise.ListAssetPropertiesHttp,
210 IoTSiteWise.BatchPutAssetPropertyValueHttp,
211 IoTSiteWise.GetAssetPropertyValueHttp,
212 IoTSiteWise.GetAssetPropertyValueHistoryHttp,
213 IoTSiteWise.GetAssetPropertyAggregatesHttp,
214 IoTSiteWise.GetInterpolatedAssetPropertyValuesHttp,
215 IoTSiteWise.ExecuteQueryHttp,
216 ),
217 ),
218 ),
219);