1import * as IoTFleetWise from "@/AWS/IoTFleetWise";
2import * as Lambda from "@/AWS/Lambda";
3import { Bucket } from "@/AWS/S3";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "bindings-handler.ts");
11
12// Deterministic bucket name (constant per test suite — never Date.now())
13// so the FleetWise bucket policy can reference the ARN as a literal.
14const TELEMETRY_BUCKET = "alchemy-test-iotfleetwise-bindings";
15
16export class FleetWiseBindingsFunction extends Lambda.Function<Lambda.Function>()(
17 "FleetWiseBindingsFunction",
18) {}
19
20export default FleetWiseBindingsFunction.make(
21 {
22 main,
23 functionUrl: true,
24 },
25 Effect.gen(function* () {
26 const catalog = yield* IoTFleetWise.SignalCatalog("BindingsSignals", {
27 nodes: [
28 { branch: { fullyQualifiedName: "Vehicle" } },
29 {
30 sensor: {
31 fullyQualifiedName: "Vehicle.Speed",
32 dataType: "DOUBLE",
33 unit: "km/h",
34 },
35 },
36 {
37 attribute: {
38 fullyQualifiedName: "Vehicle.VIN",
39 dataType: "STRING",
40 },
41 },
42 ],
43 });
44
45 const model = yield* IoTFleetWise.ModelManifest("BindingsModel", {
46 signalCatalogArn: catalog.signalCatalogArn,
47 nodes: ["Vehicle.Speed", "Vehicle.VIN"],
48 status: "ACTIVE",
49 });
50
51 const decoder = yield* IoTFleetWise.DecoderManifest("BindingsDecoder", {
52 modelManifestArn: model.modelManifestArn,
53 networkInterfaces: [
54 {
55 interfaceId: "obd0",
56 type: "OBD_INTERFACE",
57 obdInterface: { name: "obd", requestMessageId: 2015 },
58 },
59 ],
60 signalDecoders: [
61 {
62 fullyQualifiedName: "Vehicle.Speed",
63 type: "OBD_SIGNAL",
64 interfaceId: "obd0",
65 obdSignal: {
66 pidResponseLength: 1,
67 serviceMode: 1,
68 pid: 13,
69 scaling: 1,
70 offset: 0,
71 startByte: 0,
72 byteLength: 1,
73 },
74 },
75 ],
76 status: "ACTIVE",
77 });
78
79 const fleet = yield* IoTFleetWise.Fleet("BindingsFleet", {
80 signalCatalogArn: catalog.signalCatalogArn,
81 });
82
83 const vehicle = yield* IoTFleetWise.Vehicle("BindingsVehicle", {
84 modelManifestArn: model.modelManifestArn,
85 decoderManifestArn: decoder.decoderManifestArn,
86 attributes: { "Vehicle.VIN": "1HGBH41JXMN109186" },
87 });
88
89 const bucket = yield* Bucket("BindingsTelemetryBucket", {
90 bucketName: TELEMETRY_BUCKET,
91 forceDestroy: true,
92 policy: [
93 {
94 Effect: "Allow",
95 Principal: { Service: "iotfleetwise.amazonaws.com" },
96 Action: ["s3:GetBucketLocation", "s3:PutObject"],
97 Resource: [
98 `arn:aws:s3:::${TELEMETRY_BUCKET}`,
99 `arn:aws:s3:::${TELEMETRY_BUCKET}/*`,
100 ],
101 },
102 ],
103 });
104
105 // Auto-approved so the campaign is RUNNING and the SUSPEND/RESUME
106 // control route exercises real state transitions.
107 const campaign = yield* IoTFleetWise.Campaign("BindingsCampaign", {
108 signalCatalogArn: catalog.signalCatalogArn,
109 targetArn: fleet.fleetArn,
110 collectionScheme: {
111 timeBasedCollectionScheme: { period: "10 seconds" },
112 },
113 signalsToCollect: [{ name: "Vehicle.Speed" }],
114 dataDestinationConfigs: [{ s3Config: { bucketArn: bucket.bucketArn } }],
115 autoApprove: true,
116 });
117
118 const VehicleName = yield* vehicle.vehicleName;
119 const ModelManifestArn = yield* model.modelManifestArn;
120
121 const getVehicleStatus = yield* IoTFleetWise.GetVehicleStatus(vehicle);
122 const listFleetsForVehicle =
123 yield* IoTFleetWise.ListFleetsForVehicle(vehicle);
124 const listVehiclesInFleet = yield* IoTFleetWise.ListVehiclesInFleet(fleet);
125 const associateVehicleFleet =
126 yield* IoTFleetWise.AssociateVehicleFleet(fleet);
127 const disassociateVehicleFleet =
128 yield* IoTFleetWise.DisassociateVehicleFleet(fleet);
129 const updateCampaign = yield* IoTFleetWise.UpdateCampaign(campaign);
130 const listSignalCatalogNodes =
131 yield* IoTFleetWise.ListSignalCatalogNodes(catalog);
132 const listModelManifestNodes =
133 yield* IoTFleetWise.ListModelManifestNodes(model);
134 const listDecoderManifestNetworkInterfaces =
135 yield* IoTFleetWise.ListDecoderManifestNetworkInterfaces(decoder);
136 const listDecoderManifestSignals =
137 yield* IoTFleetWise.ListDecoderManifestSignals(decoder);
138 const batchCreateVehicle = yield* IoTFleetWise.BatchCreateVehicle();
139 const batchUpdateVehicle = yield* IoTFleetWise.BatchUpdateVehicle();
140 const listVehicles = yield* IoTFleetWise.ListVehicles();
141
142 const bound = {
143 getVehicleStatus,
144 listFleetsForVehicle,
145 listVehiclesInFleet,
146 associateVehicleFleet,
147 disassociateVehicleFleet,
148 updateCampaign,
149 listSignalCatalogNodes,
150 listModelManifestNodes,
151 listDecoderManifestNetworkInterfaces,
152 listDecoderManifestSignals,
153 batchCreateVehicle,
154 batchUpdateVehicle,
155 listVehicles,
156 };
157
158 return {
159 fetch: Effect.gen(function* () {
160 const request = yield* HttpServerRequest;
161 const url = new URL(request.originalUrl);
162 const pathname = url.pathname;
163
164 if (request.method === "GET" && pathname === "/bindings") {
165 return yield* HttpServerResponse.json({
166 bound: Object.keys(bound),
167 });
168 }
169
170 if (request.method === "GET" && pathname === "/vehicle-status") {
171 const response = yield* getVehicleStatus();
172 return yield* HttpServerResponse.json({
173 campaigns: (response.campaigns ?? []).length,
174 });
175 }
176
177 if (request.method === "GET" && pathname === "/vehicle-fleets") {
178 const response = yield* listFleetsForVehicle();
179 return yield* HttpServerResponse.json({
180 count: (response.fleets ?? []).length,
181 });
182 }
183
184 if (request.method === "GET" && pathname === "/fleet-vehicles") {
185 const response = yield* listVehiclesInFleet();
186 return yield* HttpServerResponse.json({
187 count: (response.vehicles ?? []).length,
188 });
189 }
190
191 if (request.method === "POST" && pathname === "/fleet/associate") {
192 const vehicleName = yield* VehicleName;
193 yield* associateVehicleFleet({ vehicleName });
194 const inFleet = yield* listVehiclesInFleet();
195 return yield* HttpServerResponse.json({
196 count: (inFleet.vehicles ?? []).length,
197 });
198 }
199
200 if (request.method === "POST" && pathname === "/fleet/disassociate") {
201 const vehicleName = yield* VehicleName;
202 yield* disassociateVehicleFleet({ vehicleName });
203 return yield* HttpServerResponse.json({ ok: true });
204 }
205
206 if (request.method === "GET" && pathname === "/catalog-nodes") {
207 const response = yield* listSignalCatalogNodes();
208 return yield* HttpServerResponse.json({
209 count: (response.nodes ?? []).length,
210 });
211 }
212
213 if (request.method === "GET" && pathname === "/model-nodes") {
214 const response = yield* listModelManifestNodes();
215 return yield* HttpServerResponse.json({
216 count: (response.nodes ?? []).length,
217 });
218 }
219
220 if (request.method === "GET" && pathname === "/decoder-interfaces") {
221 const response = yield* listDecoderManifestNetworkInterfaces();
222 return yield* HttpServerResponse.json({
223 count: (response.networkInterfaces ?? []).length,
224 });
225 }
226
227 if (request.method === "GET" && pathname === "/decoder-signals") {
228 const response = yield* listDecoderManifestSignals();
229 return yield* HttpServerResponse.json({
230 count: (response.signalDecoders ?? []).length,
231 });
232 }
233
234 if (request.method === "GET" && pathname === "/vehicles") {
235 const response = yield* listVehicles({
236 modelManifestArn: yield* ModelManifestArn,
237 });
238 return yield* HttpServerResponse.json({
239 count: (response.vehicleSummaries ?? []).length,
240 });
241 }
242
243 if (
244 request.method === "POST" &&
245 pathname === "/campaign/suspend-resume"
246 ) {
247 yield* updateCampaign({ action: "SUSPEND" });
248 yield* updateCampaign({ action: "RESUME" });
249 return yield* HttpServerResponse.json({ ok: true });
250 }
251
252 if (request.method === "POST" && pathname === "/vehicles/batch") {
253 const vehicleName = yield* VehicleName;
254 // Per-item success: merge an attribute onto the fixture vehicle.
255 const updated = yield* batchUpdateVehicle({
256 vehicles: [
257 {
258 vehicleName,
259 attributes: { "Vehicle.VIN": "1HGBH41JXMN109186" },
260 attributeUpdateMode: "Merge",
261 },
262 ],
263 });
264 // Per-item failure (bogus manifest ARN) — returned in `errors`,
265 // never creating an orphan vehicle. Proves the grant end-to-end.
266 const created = yield* batchCreateVehicle({
267 vehicles: [
268 {
269 vehicleName: "alchemy-bindings-bogus-vehicle",
270 modelManifestArn:
271 "arn:aws:iotfleetwise:us-east-1:000000000000:model-manifest/nonexistent",
272 decoderManifestArn:
273 "arn:aws:iotfleetwise:us-east-1:000000000000:decoder-manifest/nonexistent",
274 },
275 ],
276 });
277 return yield* HttpServerResponse.json({
278 updated: (updated.vehicles ?? []).length,
279 updateErrors: (updated.errors ?? []).length,
280 createErrors: (created.errors ?? []).length,
281 });
282 }
283
284 return yield* HttpServerResponse.json(
285 { error: "Not found", method: request.method, pathname },
286 { status: 404 },
287 );
288 }).pipe(Effect.orDie),
289 };
290 }).pipe(
291 Effect.provide(
292 Layer.mergeAll(
293 IoTFleetWise.GetVehicleStatusHttp,
294 IoTFleetWise.ListFleetsForVehicleHttp,
295 IoTFleetWise.ListVehiclesInFleetHttp,
296 IoTFleetWise.AssociateVehicleFleetHttp,
297 IoTFleetWise.DisassociateVehicleFleetHttp,
298 IoTFleetWise.UpdateCampaignHttp,
299 IoTFleetWise.ListSignalCatalogNodesHttp,
300 IoTFleetWise.ListModelManifestNodesHttp,
301 IoTFleetWise.ListDecoderManifestNetworkInterfacesHttp,
302 IoTFleetWise.ListDecoderManifestSignalsHttp,
303 IoTFleetWise.BatchCreateVehicleHttp,
304 IoTFleetWise.BatchUpdateVehicleHttp,
305 IoTFleetWise.ListVehiclesHttp,
306 ),
307 ),
308 ),
309);