@openfn/language-varo 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -4
- package/ast.json +119 -1
- package/dist/index.cjs +166 -83
- package/dist/index.js +164 -83
- package/package.json +3 -2
- package/types/Adaptor.d.ts +46 -0
- package/types/Utils.d.ts +1 -0
- package/types/VaroEmsUtils.d.ts +8 -1
package/README.md
CHANGED
|
@@ -239,9 +239,9 @@ openfn workflow.json -m --only convertToEms
|
|
|
239
239
|
|
|
240
240
|
Some workflows required authorization to access the resources.
|
|
241
241
|
|
|
242
|
-
## Gmail token
|
|
242
|
+
## Gmail access token
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
An access token is required to access the Gmail API. This short-lived token will last 60 minutes and will have to be manually refreshed. See the documentation in the [Gmail adaptor readme](https://docs.openfn.org/adaptors/packages/gmail-readme#retrieve-an-access-token) for a guide on how to use Google Developers OAuth 2.0 Playground to retrieve and refresh the access token.
|
|
245
245
|
|
|
246
246
|
## OpenFn collections token
|
|
247
247
|
|
|
@@ -322,10 +322,94 @@ pnpm build
|
|
|
322
322
|
pnpm run setup
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
-
### Switch to
|
|
325
|
+
### Switch to a working branch of the varo adaptor
|
|
326
326
|
|
|
327
327
|
```
|
|
328
328
|
cd openfn/adaptors/adaptors
|
|
329
|
-
git checkout
|
|
329
|
+
git checkout -b varo-enhancements
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
+
# FridgeTag `records` vs. `zReports`
|
|
333
|
+
|
|
334
|
+
## Purpose
|
|
335
|
+
|
|
336
|
+
The FridgeTag parser (`parseFridgeTagToReport`) reads structured device output and emits two parallel data structures: `records` and `zReports`. This dual design allows strict compliance with EMS standards, while also preserving valuable out-of-spec information from the original FridgeTag device logs.
|
|
337
|
+
|
|
338
|
+
## `records`: EMS-compliant daily extremes
|
|
339
|
+
|
|
340
|
+
FridgeTag source data provides daily minimum and maximum temperatures, including the exact timestamp each was observed. These data points are directly compatible with EMS requirements, which demand time-stamped records representing sensor readings.
|
|
341
|
+
|
|
342
|
+
For each day in the log, the parser generates:
|
|
343
|
+
|
|
344
|
+
- One EMS record for the minimum temperature, and
|
|
345
|
+
- One EMS record for the maximum temperature.
|
|
346
|
+
|
|
347
|
+
Each record includes:
|
|
348
|
+
- `ABST`: Absolute ISO-8601 timestamp (e.g., `"2024-10-08T08:15:00.000Z"`).
|
|
349
|
+
- `TVC`: Temperature value in °C (e.g., `18.5`).
|
|
350
|
+
- `ALRM`: Optional alarm flag (e.g., `"HEAT"` or `"FRZE"`), derived from alarm conditions.
|
|
351
|
+
- `zdescription`: A label for context (e.g., `"2024-10-08 Min T"`).
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"ABST": "2024-10-08T08:15:00.000Z",
|
|
357
|
+
"TVC": 18.5,
|
|
358
|
+
"ALRM": null,
|
|
359
|
+
"zdescription": "2024-10-08 Min T"
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
These entries fully conform to EMS specifications and can be directly integrated into compliant pipelines or summaries.
|
|
364
|
+
|
|
365
|
+
## `zReports`: Out-of-spec aggregates & alarm summaries
|
|
366
|
+
|
|
367
|
+
In addition to min/max values, FridgeTag records include:
|
|
368
|
+
|
|
369
|
+
- A daily average temperature,
|
|
370
|
+
- Detailed alarm metadata: including duration of condition (`t Acc`), first alarm timestamp (`TS A`), and alarm count (`C A`).
|
|
371
|
+
|
|
372
|
+
This information is not compatible with EMS formats, which don't align with aggregated statistics and rich alarm metadata. However, this data remains operationally meaningful, especially for:
|
|
373
|
+
|
|
374
|
+
- 60-day summary reports,
|
|
375
|
+
- Country-level immunization program dashboards,
|
|
376
|
+
- Quick on-site reviews by technicians.
|
|
377
|
+
|
|
378
|
+
To retain this data without violating EMS constraints, the parser generates a `zReports` array. Each entry summarizes one day:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"date": "2024-10-08T00:00:00.000Z",
|
|
383
|
+
"duration": "1D",
|
|
384
|
+
"alarms": [
|
|
385
|
+
{
|
|
386
|
+
"condition": "HEAT",
|
|
387
|
+
"conditionMinutes": 840,
|
|
388
|
+
"alarmTime": "2024-10-08T00:00:00.000Z"
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
"aggregates": [
|
|
392
|
+
{
|
|
393
|
+
"id": "TVC",
|
|
394
|
+
"min": 18.5,
|
|
395
|
+
"max": 21.2,
|
|
396
|
+
"average": 19.2
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
This structure is deliberately out-of-spec (hence the `z` prefix) and is ignored by EMS consumers. It is retained only for dashboards, exports, and enriched user-facing analytics.
|
|
403
|
+
|
|
404
|
+
## Design philosophy
|
|
405
|
+
|
|
406
|
+
This split structure reflects a disciplined compromise between compliance and pragmatism:
|
|
407
|
+
|
|
408
|
+
- `records`: strictly EMS-compliant atomic data points.
|
|
409
|
+
- `zReports`: high-value extras that don't align with EMS, but still provide value.
|
|
410
|
+
|
|
411
|
+
This ensures that:
|
|
412
|
+
|
|
413
|
+
- Nothing is lost from the original FridgeTag data.
|
|
414
|
+
- Downstream EMS systems remain unaffected.
|
|
415
|
+
- Local insights and user-friendly summaries remain rich and actionable.
|
package/ast.json
CHANGED
|
@@ -166,6 +166,124 @@
|
|
|
166
166
|
]
|
|
167
167
|
},
|
|
168
168
|
"valid": true
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "parseUtcForDataRange",
|
|
172
|
+
"params": [
|
|
173
|
+
"timeZone",
|
|
174
|
+
"startIso",
|
|
175
|
+
"endIso"
|
|
176
|
+
],
|
|
177
|
+
"docs": {
|
|
178
|
+
"description": "Computes the UTC datetime range that corresponds to a given IANA timezone.",
|
|
179
|
+
"tags": [
|
|
180
|
+
{
|
|
181
|
+
"title": "public",
|
|
182
|
+
"description": null,
|
|
183
|
+
"type": null
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"title": "function",
|
|
187
|
+
"description": null,
|
|
188
|
+
"name": null
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"title": "param",
|
|
192
|
+
"description": "An IANA time zone identifier (e.g. \"America/Los_Angeles\").",
|
|
193
|
+
"type": {
|
|
194
|
+
"type": "NameExpression",
|
|
195
|
+
"name": "string"
|
|
196
|
+
},
|
|
197
|
+
"name": "timeZone"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"title": "param",
|
|
201
|
+
"description": "Starting date in ISO format.",
|
|
202
|
+
"type": {
|
|
203
|
+
"type": "NameExpression",
|
|
204
|
+
"name": "string"
|
|
205
|
+
},
|
|
206
|
+
"name": "startIso"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"title": "param",
|
|
210
|
+
"description": "Ending date range in ISO format.",
|
|
211
|
+
"type": {
|
|
212
|
+
"type": "NameExpression",
|
|
213
|
+
"name": "string"
|
|
214
|
+
},
|
|
215
|
+
"name": "endIso"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"title": "returns",
|
|
219
|
+
"description": null,
|
|
220
|
+
"type": {
|
|
221
|
+
"type": "NameExpression",
|
|
222
|
+
"name": "UtcRange"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
"valid": true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "isKeyInRange",
|
|
231
|
+
"params": [
|
|
232
|
+
"key",
|
|
233
|
+
"start",
|
|
234
|
+
"end"
|
|
235
|
+
],
|
|
236
|
+
"docs": {
|
|
237
|
+
"description": "Checks whether the timestamp embedded in a key falls within a UTC datetime range.",
|
|
238
|
+
"tags": [
|
|
239
|
+
{
|
|
240
|
+
"title": "public",
|
|
241
|
+
"description": null,
|
|
242
|
+
"type": null
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"title": "function",
|
|
246
|
+
"description": null,
|
|
247
|
+
"name": null
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"title": "param",
|
|
251
|
+
"description": "A string key containing a UTC timestamp in the format `YYYYMMDDTHHMMSS`, following a colon (e.g. \"prefix:20250624T101530\").",
|
|
252
|
+
"type": {
|
|
253
|
+
"type": "NameExpression",
|
|
254
|
+
"name": "string"
|
|
255
|
+
},
|
|
256
|
+
"name": "key"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"title": "param",
|
|
260
|
+
"description": "The inclusive lower bound of the UTC datetime range.",
|
|
261
|
+
"type": {
|
|
262
|
+
"type": "NameExpression",
|
|
263
|
+
"name": "Date"
|
|
264
|
+
},
|
|
265
|
+
"name": "start"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"title": "param",
|
|
269
|
+
"description": "The exclusive upper bound of the UTC datetime range.",
|
|
270
|
+
"type": {
|
|
271
|
+
"type": "NameExpression",
|
|
272
|
+
"name": "Date"
|
|
273
|
+
},
|
|
274
|
+
"name": "end"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"title": "returns",
|
|
278
|
+
"description": "True if the parsed UTC timestamp is within the range, false otherwise.",
|
|
279
|
+
"type": {
|
|
280
|
+
"type": "NameExpression",
|
|
281
|
+
"name": "boolean"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
"valid": true
|
|
169
287
|
}
|
|
170
288
|
],
|
|
171
289
|
"exports": [],
|
|
@@ -220,7 +338,7 @@
|
|
|
220
338
|
"operation"
|
|
221
339
|
],
|
|
222
340
|
"docs": {
|
|
223
|
-
"description": "
|
|
341
|
+
"description": "Execute a function only when the condition returns true",
|
|
224
342
|
"tags": [
|
|
225
343
|
{
|
|
226
344
|
"title": "public",
|
package/dist/index.cjs
CHANGED
|
@@ -33,8 +33,10 @@ __export(src_exports, {
|
|
|
33
33
|
fields: () => import_language_common2.fields,
|
|
34
34
|
fn: () => import_language_common2.fn,
|
|
35
35
|
fnIf: () => import_language_common2.fnIf,
|
|
36
|
+
isKeyInRange: () => isKeyInRange,
|
|
36
37
|
lastReferenceValue: () => import_language_common2.lastReferenceValue,
|
|
37
38
|
merge: () => import_language_common2.merge,
|
|
39
|
+
parseUtcForDataRange: () => parseUtcForDataRange,
|
|
38
40
|
sourceValue: () => import_language_common2.sourceValue
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -55,12 +57,15 @@ __export(Adaptor_exports, {
|
|
|
55
57
|
fields: () => import_language_common2.fields,
|
|
56
58
|
fn: () => import_language_common2.fn,
|
|
57
59
|
fnIf: () => import_language_common2.fnIf,
|
|
60
|
+
isKeyInRange: () => isKeyInRange,
|
|
58
61
|
lastReferenceValue: () => import_language_common2.lastReferenceValue,
|
|
59
62
|
merge: () => import_language_common2.merge,
|
|
63
|
+
parseUtcForDataRange: () => parseUtcForDataRange,
|
|
60
64
|
sourceValue: () => import_language_common2.sourceValue
|
|
61
65
|
});
|
|
62
66
|
var import_language_common = require("@openfn/language-common");
|
|
63
67
|
var import_util = require("@openfn/language-common/util");
|
|
68
|
+
var import_luxon = require("luxon");
|
|
64
69
|
|
|
65
70
|
// src/Utils.js
|
|
66
71
|
function parseMetadata(message) {
|
|
@@ -79,7 +84,7 @@ function parseMetadata(message) {
|
|
|
79
84
|
}
|
|
80
85
|
function removeNullProps(obj) {
|
|
81
86
|
for (const key in obj) {
|
|
82
|
-
if (obj[key]
|
|
87
|
+
if (obj[key] === null) {
|
|
83
88
|
delete obj[key];
|
|
84
89
|
}
|
|
85
90
|
}
|
|
@@ -126,7 +131,18 @@ function formatDeviceInfo(data) {
|
|
|
126
131
|
output += formatType("Appliance", data.AMFR, data.AMOD, data.ASER);
|
|
127
132
|
output += formatType("Logger", data.LMFR, data.LMOD, data.LSER);
|
|
128
133
|
output += formatType("EMD", data.EMFR, data.EMOD, data.ESER);
|
|
129
|
-
return output || "
|
|
134
|
+
return output || "Cannot determine device info; no valid data found.";
|
|
135
|
+
}
|
|
136
|
+
function abbreviatedIsoToDate(iso) {
|
|
137
|
+
const [year, month, day, hour, minute, second] = [
|
|
138
|
+
iso.slice(0, 4),
|
|
139
|
+
iso.slice(4, 6),
|
|
140
|
+
iso.slice(6, 8),
|
|
141
|
+
iso.slice(9, 11),
|
|
142
|
+
iso.slice(11, 13),
|
|
143
|
+
iso.slice(13, 15)
|
|
144
|
+
];
|
|
145
|
+
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
// src/StreamingUtils.js
|
|
@@ -202,7 +218,6 @@ function mergeRecords(records, groupKey) {
|
|
|
202
218
|
}
|
|
203
219
|
if (tambAlrm != null) {
|
|
204
220
|
mergedRecord["zTambAlrm"] = tambAlrm;
|
|
205
|
-
mergedRecord["ALRM"] = tambAlrm;
|
|
206
221
|
}
|
|
207
222
|
if (tvcAlrm != null) {
|
|
208
223
|
mergedRecord["zTvcAlrm"] = tvcAlrm;
|
|
@@ -315,7 +330,7 @@ function promoteDeviceProperties(source, destination) {
|
|
|
315
330
|
}
|
|
316
331
|
|
|
317
332
|
// src/VaroEmsUtils.js
|
|
318
|
-
function parseVaroEmsToReport(metadata, data,
|
|
333
|
+
function parseVaroEmsToReport(metadata, data, rtcwMaps) {
|
|
319
334
|
const report = {
|
|
320
335
|
CID: null,
|
|
321
336
|
LAT: metadata.location.used.latitude,
|
|
@@ -339,10 +354,14 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
339
354
|
LSV: data.LSV,
|
|
340
355
|
records: []
|
|
341
356
|
};
|
|
342
|
-
|
|
357
|
+
removeNullProps(report);
|
|
343
358
|
for (const item of data.records) {
|
|
344
|
-
const
|
|
345
|
-
|
|
359
|
+
const deviceDate = rtcwMaps.get(item.RTCW);
|
|
360
|
+
if (!deviceDate) {
|
|
361
|
+
report.zHasUnreconciledRtcw = true;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const absoluteDate = applyDurationToDate(deviceDate, item.RELT);
|
|
346
365
|
const abst = parseIsoToAbbreviatedIso(absoluteDate);
|
|
347
366
|
const record = {
|
|
348
367
|
ABST: abst,
|
|
@@ -359,67 +378,82 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
359
378
|
TAMB: item.TAMB,
|
|
360
379
|
TVC: item.TVC
|
|
361
380
|
};
|
|
381
|
+
removeNullProps(record);
|
|
362
382
|
report.records.push(record);
|
|
363
383
|
}
|
|
364
384
|
return report;
|
|
365
385
|
}
|
|
366
|
-
function
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
if (!
|
|
370
|
-
|
|
386
|
+
function applyDurationToDate(incomingDate, duration, subtract = false) {
|
|
387
|
+
const date = normalizeIncomingDate(incomingDate);
|
|
388
|
+
const parsedDuration = parseDuration(duration, subtract);
|
|
389
|
+
if (!parsedDuration)
|
|
390
|
+
return date;
|
|
391
|
+
date.setUTCDate(date.getUTCDate() + parsedDuration.days);
|
|
392
|
+
date.setUTCHours(date.getUTCHours() + parsedDuration.hours);
|
|
393
|
+
date.setUTCMinutes(date.getUTCMinutes() + parsedDuration.minutes);
|
|
394
|
+
date.setUTCSeconds(date.getUTCSeconds() + parsedDuration.seconds);
|
|
395
|
+
return date;
|
|
396
|
+
}
|
|
397
|
+
function parseDuration(duration, subtract) {
|
|
398
|
+
const regex = /^P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+)S)?)?$/;
|
|
399
|
+
const match = duration.match(regex);
|
|
400
|
+
if (!match)
|
|
401
|
+
throw new Error(`Invalid duration format: ${duration}`);
|
|
402
|
+
const m = subtract ? -1 : 1;
|
|
403
|
+
const days = +(match.groups.days || 0) * m;
|
|
404
|
+
const hours = +(match.groups.hours || 0) * m;
|
|
405
|
+
const minutes = +(match.groups.minutes || 0) * m;
|
|
406
|
+
const seconds = +(match.groups.seconds || 0) * m;
|
|
407
|
+
return days || hours || minutes || seconds ? { days, hours, minutes, seconds } : null;
|
|
408
|
+
}
|
|
409
|
+
function normalizeIncomingDate(incomingDate) {
|
|
410
|
+
let date = incomingDate;
|
|
411
|
+
if (typeof date === "string" && /^\d{8}T\d{6}Z$/.test(date)) {
|
|
412
|
+
date = parseAbbreviatedIsoToIso(date);
|
|
371
413
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
414
|
+
return new Date(date);
|
|
415
|
+
}
|
|
416
|
+
function parseAbbreviatedIsoToIso(abbrIso) {
|
|
417
|
+
const m = abbrIso.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
|
|
418
|
+
if (!m)
|
|
419
|
+
throw new Error(`Invalid abbreviated ISO date format: ${abbrIso}`);
|
|
420
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
|
|
421
|
+
}
|
|
422
|
+
function parseIsoToAbbreviatedIso(iso) {
|
|
423
|
+
return iso.toISOString().replace(/[\-\:]/g, "").replace(".000Z", "Z");
|
|
424
|
+
}
|
|
425
|
+
function buildDeviceRtcwDateMaps(contents) {
|
|
426
|
+
const dataContents = contents.filter((c) => c.data);
|
|
427
|
+
const deviceRtcwDateMaps = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const content of dataContents) {
|
|
429
|
+
const { deviceId, deviceDate, finalRtcw } = extractDeviceData(content);
|
|
430
|
+
let rtcwDateMap = deviceRtcwDateMaps.get(deviceId);
|
|
431
|
+
if (!rtcwDateMap) {
|
|
432
|
+
rtcwDateMap = /* @__PURE__ */ new Map();
|
|
433
|
+
deviceRtcwDateMaps.set(deviceId, rtcwDateMap);
|
|
385
434
|
}
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const minutes = parseInt(match.groups.minutes || 0, 10) * multiplier;
|
|
390
|
-
const seconds = parseInt(match.groups.seconds || 0, 10) * multiplier;
|
|
391
|
-
if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) {
|
|
392
|
-
return null;
|
|
435
|
+
const existingDate = rtcwDateMap.get(finalRtcw);
|
|
436
|
+
if (!existingDate || deviceDate < existingDate) {
|
|
437
|
+
rtcwDateMap.set(finalRtcw, deviceDate);
|
|
393
438
|
}
|
|
394
|
-
return { days, hours, minutes, seconds };
|
|
395
439
|
}
|
|
396
|
-
|
|
397
|
-
date.setDate(date.getDate() + duration.days);
|
|
398
|
-
date.setHours(date.getHours() + duration.hours);
|
|
399
|
-
date.setMinutes(date.getMinutes() + duration.minutes);
|
|
400
|
-
date.setSeconds(date.getSeconds() + duration.seconds);
|
|
401
|
-
}
|
|
402
|
-
const adjustedDate = new Date(incomingDate);
|
|
403
|
-
for (const duration of durations) {
|
|
404
|
-
if (!duration)
|
|
405
|
-
continue;
|
|
406
|
-
const parsedDuration = parseDuration(duration);
|
|
407
|
-
if (!parsedDuration)
|
|
408
|
-
continue;
|
|
409
|
-
applyDuration(adjustedDate, parsedDuration);
|
|
410
|
-
}
|
|
411
|
-
return adjustedDate;
|
|
440
|
+
return deviceRtcwDateMaps;
|
|
412
441
|
}
|
|
413
|
-
function
|
|
414
|
-
const regex = /^(
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
442
|
+
function extractDeviceData(content) {
|
|
443
|
+
const regex = /^([a-zA-Z0-9]+)_(?:.+)_([A-Z0-9]{4,15})_([0-9]{8}T[0-9]{6}Z)\.json$/;
|
|
444
|
+
const [, deviceId, deviceRelt, abbrUsbDate] = content.data.filename.match(regex);
|
|
445
|
+
const deviceDate = applyDurationToDate(abbrUsbDate, deviceRelt, true);
|
|
446
|
+
if (typeof content.data.content === "string") {
|
|
447
|
+
try {
|
|
448
|
+
content.data.content = JSON.parse(content.data.content);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
console.error("Invalid JSON string in content.data.content:", e);
|
|
451
|
+
}
|
|
418
452
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return
|
|
453
|
+
content.zDeviceId = deviceId;
|
|
454
|
+
const data = content.data.content;
|
|
455
|
+
const finalRtcw = data.records[data.records.length - 1].RTCW;
|
|
456
|
+
return { deviceId, deviceDate, finalRtcw };
|
|
423
457
|
}
|
|
424
458
|
|
|
425
459
|
// src/FridgeTagUtils.js
|
|
@@ -479,7 +513,7 @@ function parseFridgeTagToReport(metadata, nodes) {
|
|
|
479
513
|
ABST: dateTime,
|
|
480
514
|
TVC: temp,
|
|
481
515
|
ALRM: alarm,
|
|
482
|
-
|
|
516
|
+
zDescription: date + " " + tempField
|
|
483
517
|
});
|
|
484
518
|
function parseAlarm(alarmNode, description) {
|
|
485
519
|
if (alarmNode["t Acc"] === "0")
|
|
@@ -577,38 +611,46 @@ function parseFridgeTag(text) {
|
|
|
577
611
|
var import_language_common2 = require("@openfn/language-common");
|
|
578
612
|
function convertToEms(messageContents) {
|
|
579
613
|
return async (state) => {
|
|
580
|
-
var _a, _b;
|
|
581
614
|
const [resolvedMessageContents] = (0, import_util.expandReferences)(state, messageContents);
|
|
582
615
|
const reports = [];
|
|
583
|
-
console.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (!metadata)
|
|
588
|
-
continue;
|
|
589
|
-
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
590
|
-
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
591
|
-
reports.push(result);
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
if ((_b = content.data) == null ? void 0 : _b.content) {
|
|
595
|
-
const metadata = parseMetadata(content);
|
|
596
|
-
if (!metadata)
|
|
597
|
-
continue;
|
|
598
|
-
const data = JSON.parse(content.data.content);
|
|
599
|
-
const dataPath2 = content.data.filename;
|
|
600
|
-
const result = parseVaroEmsToReport(metadata, data, dataPath2);
|
|
601
|
-
reports.push(result);
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
616
|
+
console.info("Incoming message contents", resolvedMessageContents.length);
|
|
617
|
+
processFridgeTagContents(resolvedMessageContents, reports);
|
|
618
|
+
processDataContents(resolvedMessageContents, reports);
|
|
619
|
+
for (const content of resolvedMessageContents.filter((c) => !c.zProcessed)) {
|
|
604
620
|
console.error(
|
|
605
621
|
`Insufficient content found for MessageID: ${content.messageId}`
|
|
606
622
|
);
|
|
607
623
|
}
|
|
608
|
-
console.
|
|
624
|
+
console.info("Converted message contents", reports.length);
|
|
609
625
|
return { ...(0, import_language_common.composeNextState)(state, reports) };
|
|
610
626
|
};
|
|
611
627
|
}
|
|
628
|
+
function processFridgeTagContents(contents, reports) {
|
|
629
|
+
const fridgeTagContents = contents.filter((c) => c.fridgeTag);
|
|
630
|
+
for (const content of fridgeTagContents) {
|
|
631
|
+
const metadata = parseMetadata(content);
|
|
632
|
+
if (!metadata)
|
|
633
|
+
continue;
|
|
634
|
+
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
635
|
+
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
636
|
+
reports.push(result);
|
|
637
|
+
content.zProcessed = true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function processDataContents(contents, reports) {
|
|
641
|
+
const dataContents = contents.filter((c) => c.data);
|
|
642
|
+
const deviceRtcwDateMaps = buildDeviceRtcwDateMaps(dataContents);
|
|
643
|
+
for (const content of dataContents) {
|
|
644
|
+
const metadata = parseMetadata(content);
|
|
645
|
+
if (!metadata)
|
|
646
|
+
continue;
|
|
647
|
+
const data = content.data.content;
|
|
648
|
+
const rtcwMaps = deviceRtcwDateMaps.get(content.zDeviceId);
|
|
649
|
+
const result = parseVaroEmsToReport(metadata, data, rtcwMaps);
|
|
650
|
+
reports.push(result);
|
|
651
|
+
content.zProcessed = true;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
612
654
|
function convertItemsToReports(items, reportType = "unknown") {
|
|
613
655
|
return async (state) => {
|
|
614
656
|
const [resolvedRecords, resolvedReportType] = (0, import_util.expandReferences)(
|
|
@@ -636,7 +678,7 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
636
678
|
reportType
|
|
637
679
|
);
|
|
638
680
|
const messageContents = [];
|
|
639
|
-
for (const report of resolvedReports) {
|
|
681
|
+
for (const report of resolvedReports ?? []) {
|
|
640
682
|
report["zReportType"] = resolvedReportType;
|
|
641
683
|
report["zGeneratedTimestamp"] = new Date().toISOString();
|
|
642
684
|
const serialNumber = report["ESER"] || report["LSER"] || report["ASER"];
|
|
@@ -653,6 +695,45 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
653
695
|
return { ...(0, import_language_common.composeNextState)(state, messageContents) };
|
|
654
696
|
};
|
|
655
697
|
}
|
|
698
|
+
function parseUtcForDataRange(timeZone, startIso, endIso) {
|
|
699
|
+
const localNow = import_luxon.DateTime.now().setZone(timeZone);
|
|
700
|
+
const wallClock = new Date(
|
|
701
|
+
localNow.year,
|
|
702
|
+
localNow.month - 1,
|
|
703
|
+
localNow.day,
|
|
704
|
+
localNow.hour,
|
|
705
|
+
localNow.minute,
|
|
706
|
+
localNow.second,
|
|
707
|
+
localNow.millisecond
|
|
708
|
+
);
|
|
709
|
+
const startLocal = import_luxon.DateTime.fromISO(startIso, { zone: timeZone });
|
|
710
|
+
const endLocal = import_luxon.DateTime.fromISO(endIso, { zone: timeZone });
|
|
711
|
+
const startUtc = startLocal.toUTC().toJSDate();
|
|
712
|
+
const endUtc = endLocal.toUTC().toJSDate();
|
|
713
|
+
const daysDiff = Math.floor(endLocal.diff(startLocal, "days").days);
|
|
714
|
+
const collectionKeyPattern = "yyyyLLdd";
|
|
715
|
+
const collectionKeys = [];
|
|
716
|
+
for (let i = 0; i <= daysDiff; i++) {
|
|
717
|
+
const currentDay = startLocal.plus({ days: i });
|
|
718
|
+
collectionKeys.push(`*${currentDay.toFormat(collectionKeyPattern)}*`);
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
wallClock,
|
|
722
|
+
startUtc,
|
|
723
|
+
endUtc,
|
|
724
|
+
collectionKeys
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function isKeyInRange(key, start, end) {
|
|
728
|
+
console.error(
|
|
729
|
+
"i changed this implementation and need to verify the scenarios."
|
|
730
|
+
);
|
|
731
|
+
const iso = key == null ? void 0 : key.split(":")[1];
|
|
732
|
+
if (!iso || iso.length < 15)
|
|
733
|
+
return false;
|
|
734
|
+
const date = abbreviatedIsoToDate(iso);
|
|
735
|
+
return date >= start && date < end;
|
|
736
|
+
}
|
|
656
737
|
|
|
657
738
|
// src/index.js
|
|
658
739
|
var src_default = Adaptor_exports;
|
|
@@ -671,7 +752,9 @@ var src_default = Adaptor_exports;
|
|
|
671
752
|
fields,
|
|
672
753
|
fn,
|
|
673
754
|
fnIf,
|
|
755
|
+
isKeyInRange,
|
|
674
756
|
lastReferenceValue,
|
|
675
757
|
merge,
|
|
758
|
+
parseUtcForDataRange,
|
|
676
759
|
sourceValue
|
|
677
760
|
});
|
package/dist/index.js
CHANGED
|
@@ -20,12 +20,15 @@ __export(Adaptor_exports, {
|
|
|
20
20
|
fields: () => fields,
|
|
21
21
|
fn: () => fn,
|
|
22
22
|
fnIf: () => fnIf,
|
|
23
|
+
isKeyInRange: () => isKeyInRange,
|
|
23
24
|
lastReferenceValue: () => lastReferenceValue,
|
|
24
25
|
merge: () => merge,
|
|
26
|
+
parseUtcForDataRange: () => parseUtcForDataRange,
|
|
25
27
|
sourceValue: () => sourceValue
|
|
26
28
|
});
|
|
27
29
|
import { composeNextState } from "@openfn/language-common";
|
|
28
30
|
import { expandReferences } from "@openfn/language-common/util";
|
|
31
|
+
import { DateTime } from "luxon";
|
|
29
32
|
|
|
30
33
|
// src/Utils.js
|
|
31
34
|
function parseMetadata(message) {
|
|
@@ -44,7 +47,7 @@ function parseMetadata(message) {
|
|
|
44
47
|
}
|
|
45
48
|
function removeNullProps(obj) {
|
|
46
49
|
for (const key in obj) {
|
|
47
|
-
if (obj[key]
|
|
50
|
+
if (obj[key] === null) {
|
|
48
51
|
delete obj[key];
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -91,7 +94,18 @@ function formatDeviceInfo(data) {
|
|
|
91
94
|
output += formatType("Appliance", data.AMFR, data.AMOD, data.ASER);
|
|
92
95
|
output += formatType("Logger", data.LMFR, data.LMOD, data.LSER);
|
|
93
96
|
output += formatType("EMD", data.EMFR, data.EMOD, data.ESER);
|
|
94
|
-
return output || "
|
|
97
|
+
return output || "Cannot determine device info; no valid data found.";
|
|
98
|
+
}
|
|
99
|
+
function abbreviatedIsoToDate(iso) {
|
|
100
|
+
const [year, month, day, hour, minute, second] = [
|
|
101
|
+
iso.slice(0, 4),
|
|
102
|
+
iso.slice(4, 6),
|
|
103
|
+
iso.slice(6, 8),
|
|
104
|
+
iso.slice(9, 11),
|
|
105
|
+
iso.slice(11, 13),
|
|
106
|
+
iso.slice(13, 15)
|
|
107
|
+
];
|
|
108
|
+
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
// src/StreamingUtils.js
|
|
@@ -167,7 +181,6 @@ function mergeRecords(records, groupKey) {
|
|
|
167
181
|
}
|
|
168
182
|
if (tambAlrm != null) {
|
|
169
183
|
mergedRecord["zTambAlrm"] = tambAlrm;
|
|
170
|
-
mergedRecord["ALRM"] = tambAlrm;
|
|
171
184
|
}
|
|
172
185
|
if (tvcAlrm != null) {
|
|
173
186
|
mergedRecord["zTvcAlrm"] = tvcAlrm;
|
|
@@ -280,7 +293,7 @@ function promoteDeviceProperties(source, destination) {
|
|
|
280
293
|
}
|
|
281
294
|
|
|
282
295
|
// src/VaroEmsUtils.js
|
|
283
|
-
function parseVaroEmsToReport(metadata, data,
|
|
296
|
+
function parseVaroEmsToReport(metadata, data, rtcwMaps) {
|
|
284
297
|
const report = {
|
|
285
298
|
CID: null,
|
|
286
299
|
LAT: metadata.location.used.latitude,
|
|
@@ -304,10 +317,14 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
304
317
|
LSV: data.LSV,
|
|
305
318
|
records: []
|
|
306
319
|
};
|
|
307
|
-
|
|
320
|
+
removeNullProps(report);
|
|
308
321
|
for (const item of data.records) {
|
|
309
|
-
const
|
|
310
|
-
|
|
322
|
+
const deviceDate = rtcwMaps.get(item.RTCW);
|
|
323
|
+
if (!deviceDate) {
|
|
324
|
+
report.zHasUnreconciledRtcw = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const absoluteDate = applyDurationToDate(deviceDate, item.RELT);
|
|
311
328
|
const abst = parseIsoToAbbreviatedIso(absoluteDate);
|
|
312
329
|
const record = {
|
|
313
330
|
ABST: abst,
|
|
@@ -324,67 +341,82 @@ function parseVaroEmsToReport(metadata, data, dataPath2) {
|
|
|
324
341
|
TAMB: item.TAMB,
|
|
325
342
|
TVC: item.TVC
|
|
326
343
|
};
|
|
344
|
+
removeNullProps(record);
|
|
327
345
|
report.records.push(record);
|
|
328
346
|
}
|
|
329
347
|
return report;
|
|
330
348
|
}
|
|
331
|
-
function
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
if (!
|
|
335
|
-
|
|
349
|
+
function applyDurationToDate(incomingDate, duration, subtract = false) {
|
|
350
|
+
const date = normalizeIncomingDate(incomingDate);
|
|
351
|
+
const parsedDuration = parseDuration(duration, subtract);
|
|
352
|
+
if (!parsedDuration)
|
|
353
|
+
return date;
|
|
354
|
+
date.setUTCDate(date.getUTCDate() + parsedDuration.days);
|
|
355
|
+
date.setUTCHours(date.getUTCHours() + parsedDuration.hours);
|
|
356
|
+
date.setUTCMinutes(date.getUTCMinutes() + parsedDuration.minutes);
|
|
357
|
+
date.setUTCSeconds(date.getUTCSeconds() + parsedDuration.seconds);
|
|
358
|
+
return date;
|
|
359
|
+
}
|
|
360
|
+
function parseDuration(duration, subtract) {
|
|
361
|
+
const regex = /^P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+)S)?)?$/;
|
|
362
|
+
const match = duration.match(regex);
|
|
363
|
+
if (!match)
|
|
364
|
+
throw new Error(`Invalid duration format: ${duration}`);
|
|
365
|
+
const m = subtract ? -1 : 1;
|
|
366
|
+
const days = +(match.groups.days || 0) * m;
|
|
367
|
+
const hours = +(match.groups.hours || 0) * m;
|
|
368
|
+
const minutes = +(match.groups.minutes || 0) * m;
|
|
369
|
+
const seconds = +(match.groups.seconds || 0) * m;
|
|
370
|
+
return days || hours || minutes || seconds ? { days, hours, minutes, seconds } : null;
|
|
371
|
+
}
|
|
372
|
+
function normalizeIncomingDate(incomingDate) {
|
|
373
|
+
let date = incomingDate;
|
|
374
|
+
if (typeof date === "string" && /^\d{8}T\d{6}Z$/.test(date)) {
|
|
375
|
+
date = parseAbbreviatedIsoToIso(date);
|
|
336
376
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
377
|
+
return new Date(date);
|
|
378
|
+
}
|
|
379
|
+
function parseAbbreviatedIsoToIso(abbrIso) {
|
|
380
|
+
const m = abbrIso.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
|
|
381
|
+
if (!m)
|
|
382
|
+
throw new Error(`Invalid abbreviated ISO date format: ${abbrIso}`);
|
|
383
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
|
|
384
|
+
}
|
|
385
|
+
function parseIsoToAbbreviatedIso(iso) {
|
|
386
|
+
return iso.toISOString().replace(/[\-\:]/g, "").replace(".000Z", "Z");
|
|
387
|
+
}
|
|
388
|
+
function buildDeviceRtcwDateMaps(contents) {
|
|
389
|
+
const dataContents = contents.filter((c) => c.data);
|
|
390
|
+
const deviceRtcwDateMaps = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const content of dataContents) {
|
|
392
|
+
const { deviceId, deviceDate, finalRtcw } = extractDeviceData(content);
|
|
393
|
+
let rtcwDateMap = deviceRtcwDateMaps.get(deviceId);
|
|
394
|
+
if (!rtcwDateMap) {
|
|
395
|
+
rtcwDateMap = /* @__PURE__ */ new Map();
|
|
396
|
+
deviceRtcwDateMaps.set(deviceId, rtcwDateMap);
|
|
350
397
|
}
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const minutes = parseInt(match.groups.minutes || 0, 10) * multiplier;
|
|
355
|
-
const seconds = parseInt(match.groups.seconds || 0, 10) * multiplier;
|
|
356
|
-
if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) {
|
|
357
|
-
return null;
|
|
398
|
+
const existingDate = rtcwDateMap.get(finalRtcw);
|
|
399
|
+
if (!existingDate || deviceDate < existingDate) {
|
|
400
|
+
rtcwDateMap.set(finalRtcw, deviceDate);
|
|
358
401
|
}
|
|
359
|
-
return { days, hours, minutes, seconds };
|
|
360
402
|
}
|
|
361
|
-
|
|
362
|
-
date.setDate(date.getDate() + duration.days);
|
|
363
|
-
date.setHours(date.getHours() + duration.hours);
|
|
364
|
-
date.setMinutes(date.getMinutes() + duration.minutes);
|
|
365
|
-
date.setSeconds(date.getSeconds() + duration.seconds);
|
|
366
|
-
}
|
|
367
|
-
const adjustedDate = new Date(incomingDate);
|
|
368
|
-
for (const duration of durations) {
|
|
369
|
-
if (!duration)
|
|
370
|
-
continue;
|
|
371
|
-
const parsedDuration = parseDuration(duration);
|
|
372
|
-
if (!parsedDuration)
|
|
373
|
-
continue;
|
|
374
|
-
applyDuration(adjustedDate, parsedDuration);
|
|
375
|
-
}
|
|
376
|
-
return adjustedDate;
|
|
403
|
+
return deviceRtcwDateMaps;
|
|
377
404
|
}
|
|
378
|
-
function
|
|
379
|
-
const regex = /^(
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
405
|
+
function extractDeviceData(content) {
|
|
406
|
+
const regex = /^([a-zA-Z0-9]+)_(?:.+)_([A-Z0-9]{4,15})_([0-9]{8}T[0-9]{6}Z)\.json$/;
|
|
407
|
+
const [, deviceId, deviceRelt, abbrUsbDate] = content.data.filename.match(regex);
|
|
408
|
+
const deviceDate = applyDurationToDate(abbrUsbDate, deviceRelt, true);
|
|
409
|
+
if (typeof content.data.content === "string") {
|
|
410
|
+
try {
|
|
411
|
+
content.data.content = JSON.parse(content.data.content);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
console.error("Invalid JSON string in content.data.content:", e);
|
|
414
|
+
}
|
|
383
415
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return
|
|
416
|
+
content.zDeviceId = deviceId;
|
|
417
|
+
const data = content.data.content;
|
|
418
|
+
const finalRtcw = data.records[data.records.length - 1].RTCW;
|
|
419
|
+
return { deviceId, deviceDate, finalRtcw };
|
|
388
420
|
}
|
|
389
421
|
|
|
390
422
|
// src/FridgeTagUtils.js
|
|
@@ -444,7 +476,7 @@ function parseFridgeTagToReport(metadata, nodes) {
|
|
|
444
476
|
ABST: dateTime,
|
|
445
477
|
TVC: temp,
|
|
446
478
|
ALRM: alarm,
|
|
447
|
-
|
|
479
|
+
zDescription: date + " " + tempField
|
|
448
480
|
});
|
|
449
481
|
function parseAlarm(alarmNode, description) {
|
|
450
482
|
if (alarmNode["t Acc"] === "0")
|
|
@@ -556,38 +588,46 @@ import {
|
|
|
556
588
|
} from "@openfn/language-common";
|
|
557
589
|
function convertToEms(messageContents) {
|
|
558
590
|
return async (state) => {
|
|
559
|
-
var _a, _b;
|
|
560
591
|
const [resolvedMessageContents] = expandReferences(state, messageContents);
|
|
561
592
|
const reports = [];
|
|
562
|
-
console.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (!metadata)
|
|
567
|
-
continue;
|
|
568
|
-
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
569
|
-
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
570
|
-
reports.push(result);
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
if ((_b = content.data) == null ? void 0 : _b.content) {
|
|
574
|
-
const metadata = parseMetadata(content);
|
|
575
|
-
if (!metadata)
|
|
576
|
-
continue;
|
|
577
|
-
const data = JSON.parse(content.data.content);
|
|
578
|
-
const dataPath2 = content.data.filename;
|
|
579
|
-
const result = parseVaroEmsToReport(metadata, data, dataPath2);
|
|
580
|
-
reports.push(result);
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
593
|
+
console.info("Incoming message contents", resolvedMessageContents.length);
|
|
594
|
+
processFridgeTagContents(resolvedMessageContents, reports);
|
|
595
|
+
processDataContents(resolvedMessageContents, reports);
|
|
596
|
+
for (const content of resolvedMessageContents.filter((c) => !c.zProcessed)) {
|
|
583
597
|
console.error(
|
|
584
598
|
`Insufficient content found for MessageID: ${content.messageId}`
|
|
585
599
|
);
|
|
586
600
|
}
|
|
587
|
-
console.
|
|
601
|
+
console.info("Converted message contents", reports.length);
|
|
588
602
|
return { ...composeNextState(state, reports) };
|
|
589
603
|
};
|
|
590
604
|
}
|
|
605
|
+
function processFridgeTagContents(contents, reports) {
|
|
606
|
+
const fridgeTagContents = contents.filter((c) => c.fridgeTag);
|
|
607
|
+
for (const content of fridgeTagContents) {
|
|
608
|
+
const metadata = parseMetadata(content);
|
|
609
|
+
if (!metadata)
|
|
610
|
+
continue;
|
|
611
|
+
const fridgeTagNodes = parseFridgeTag(content.fridgeTag.content);
|
|
612
|
+
const result = parseFridgeTagToReport(metadata, fridgeTagNodes);
|
|
613
|
+
reports.push(result);
|
|
614
|
+
content.zProcessed = true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function processDataContents(contents, reports) {
|
|
618
|
+
const dataContents = contents.filter((c) => c.data);
|
|
619
|
+
const deviceRtcwDateMaps = buildDeviceRtcwDateMaps(dataContents);
|
|
620
|
+
for (const content of dataContents) {
|
|
621
|
+
const metadata = parseMetadata(content);
|
|
622
|
+
if (!metadata)
|
|
623
|
+
continue;
|
|
624
|
+
const data = content.data.content;
|
|
625
|
+
const rtcwMaps = deviceRtcwDateMaps.get(content.zDeviceId);
|
|
626
|
+
const result = parseVaroEmsToReport(metadata, data, rtcwMaps);
|
|
627
|
+
reports.push(result);
|
|
628
|
+
content.zProcessed = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
591
631
|
function convertItemsToReports(items, reportType = "unknown") {
|
|
592
632
|
return async (state) => {
|
|
593
633
|
const [resolvedRecords, resolvedReportType] = expandReferences(
|
|
@@ -615,7 +655,7 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
615
655
|
reportType
|
|
616
656
|
);
|
|
617
657
|
const messageContents = [];
|
|
618
|
-
for (const report of resolvedReports) {
|
|
658
|
+
for (const report of resolvedReports ?? []) {
|
|
619
659
|
report["zReportType"] = resolvedReportType;
|
|
620
660
|
report["zGeneratedTimestamp"] = new Date().toISOString();
|
|
621
661
|
const serialNumber = report["ESER"] || report["LSER"] || report["ASER"];
|
|
@@ -632,6 +672,45 @@ function convertReportsToMessageContents(reports, reportType = "unknown") {
|
|
|
632
672
|
return { ...composeNextState(state, messageContents) };
|
|
633
673
|
};
|
|
634
674
|
}
|
|
675
|
+
function parseUtcForDataRange(timeZone, startIso, endIso) {
|
|
676
|
+
const localNow = DateTime.now().setZone(timeZone);
|
|
677
|
+
const wallClock = new Date(
|
|
678
|
+
localNow.year,
|
|
679
|
+
localNow.month - 1,
|
|
680
|
+
localNow.day,
|
|
681
|
+
localNow.hour,
|
|
682
|
+
localNow.minute,
|
|
683
|
+
localNow.second,
|
|
684
|
+
localNow.millisecond
|
|
685
|
+
);
|
|
686
|
+
const startLocal = DateTime.fromISO(startIso, { zone: timeZone });
|
|
687
|
+
const endLocal = DateTime.fromISO(endIso, { zone: timeZone });
|
|
688
|
+
const startUtc = startLocal.toUTC().toJSDate();
|
|
689
|
+
const endUtc = endLocal.toUTC().toJSDate();
|
|
690
|
+
const daysDiff = Math.floor(endLocal.diff(startLocal, "days").days);
|
|
691
|
+
const collectionKeyPattern = "yyyyLLdd";
|
|
692
|
+
const collectionKeys = [];
|
|
693
|
+
for (let i = 0; i <= daysDiff; i++) {
|
|
694
|
+
const currentDay = startLocal.plus({ days: i });
|
|
695
|
+
collectionKeys.push(`*${currentDay.toFormat(collectionKeyPattern)}*`);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
wallClock,
|
|
699
|
+
startUtc,
|
|
700
|
+
endUtc,
|
|
701
|
+
collectionKeys
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function isKeyInRange(key, start, end) {
|
|
705
|
+
console.error(
|
|
706
|
+
"i changed this implementation and need to verify the scenarios."
|
|
707
|
+
);
|
|
708
|
+
const iso = key == null ? void 0 : key.split(":")[1];
|
|
709
|
+
if (!iso || iso.length < 15)
|
|
710
|
+
return false;
|
|
711
|
+
const date = abbreviatedIsoToDate(iso);
|
|
712
|
+
return date >= start && date < end;
|
|
713
|
+
}
|
|
635
714
|
|
|
636
715
|
// src/index.js
|
|
637
716
|
var src_default = Adaptor_exports;
|
|
@@ -650,7 +729,9 @@ export {
|
|
|
650
729
|
fields,
|
|
651
730
|
fn,
|
|
652
731
|
fnIf,
|
|
732
|
+
isKeyInRange,
|
|
653
733
|
lastReferenceValue,
|
|
654
734
|
merge,
|
|
735
|
+
parseUtcForDataRange,
|
|
655
736
|
sourceValue
|
|
656
737
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfn/language-varo",
|
|
3
3
|
"label": "Varo",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.1",
|
|
5
5
|
"description": "OpenFn varo adaptor for cold chain information",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"configuration-schema.json"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"
|
|
24
|
+
"luxon": "^3.6.1",
|
|
25
|
+
"@openfn/language-common": "3.0.3"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"assertion-error": "2.0.0",
|
package/types/Adaptor.d.ts
CHANGED
|
@@ -57,4 +57,50 @@ export function convertItemsToReports(items: any[], reportType?: string): Operat
|
|
|
57
57
|
* convertReportsToMessageContents(emsReports, "ems");
|
|
58
58
|
*/
|
|
59
59
|
export function convertReportsToMessageContents(reports: any, reportType?: string): Function;
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} UtcRange
|
|
62
|
+
* @property {Date} wallClock - The current local datetime as it appears on the wall in the specified timezone.
|
|
63
|
+
* @property {Date} startUtc - UTC start date range (inclusive).
|
|
64
|
+
* @property {Date} endUtc - UTC end of date range (exclusive).
|
|
65
|
+
* @property {Array} collectionKeys - Array of wildcard patterns to match UTC dates which correspond with date range (e.g. "*20250624*").
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Computes the UTC datetime range that corresponds to a given IANA timezone.
|
|
69
|
+
* @public
|
|
70
|
+
* @function
|
|
71
|
+
* @param {string} timeZone - An IANA time zone identifier (e.g. "America/Los_Angeles").
|
|
72
|
+
* @param {string} startIso - Starting date in ISO format.
|
|
73
|
+
* @param {string} endIso - Ending date range in ISO format.
|
|
74
|
+
* @returns {UtcRange}
|
|
75
|
+
*/
|
|
76
|
+
export function parseUtcForDataRange(timeZone: string, startIso: string, endIso: string): UtcRange;
|
|
77
|
+
/**
|
|
78
|
+
* Checks whether the timestamp embedded in a key falls within a UTC datetime range.
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
* @function
|
|
82
|
+
* @param {string} key - A string key containing a UTC timestamp in the format `YYYYMMDDTHHMMSS`, following a colon (e.g. "prefix:20250624T101530").
|
|
83
|
+
* @param {Date} start - The inclusive lower bound of the UTC datetime range.
|
|
84
|
+
* @param {Date} end - The exclusive upper bound of the UTC datetime range.
|
|
85
|
+
* @returns {boolean} True if the parsed UTC timestamp is within the range, false otherwise.
|
|
86
|
+
*/
|
|
87
|
+
export function isKeyInRange(key: string, start: Date, end: Date): boolean;
|
|
88
|
+
export type UtcRange = {
|
|
89
|
+
/**
|
|
90
|
+
* - The current local datetime as it appears on the wall in the specified timezone.
|
|
91
|
+
*/
|
|
92
|
+
wallClock: Date;
|
|
93
|
+
/**
|
|
94
|
+
* - UTC start date range (inclusive).
|
|
95
|
+
*/
|
|
96
|
+
startUtc: Date;
|
|
97
|
+
/**
|
|
98
|
+
* - UTC end of date range (exclusive).
|
|
99
|
+
*/
|
|
100
|
+
endUtc: Date;
|
|
101
|
+
/**
|
|
102
|
+
* - Array of wildcard patterns to match UTC dates which correspond with date range (e.g. "*20250624*").
|
|
103
|
+
*/
|
|
104
|
+
collectionKeys: any[];
|
|
105
|
+
};
|
|
60
106
|
export { alterState, combine, cursor, dataPath, dataValue, each, field, fields, fn, fnIf, lastReferenceValue, merge, sourceValue } from "@openfn/language-common";
|
package/types/Utils.d.ts
CHANGED
package/types/VaroEmsUtils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function parseVaroEmsToReport(metadata: any, data: any,
|
|
1
|
+
export function parseVaroEmsToReport(metadata: any, data: any, rtcwMaps: any): {
|
|
2
2
|
CID: any;
|
|
3
3
|
LAT: any;
|
|
4
4
|
LNG: any;
|
|
@@ -21,3 +21,10 @@ export function parseVaroEmsToReport(metadata: any, data: any, dataPath: any): {
|
|
|
21
21
|
LSV: any;
|
|
22
22
|
records: any[];
|
|
23
23
|
};
|
|
24
|
+
export function applyDurationToDate(incomingDate: any, duration: any, subtract?: boolean): Date;
|
|
25
|
+
export function buildDeviceRtcwDateMaps(contents: any): Map<any, any>;
|
|
26
|
+
export function extractDeviceData(content: any): {
|
|
27
|
+
deviceId: any;
|
|
28
|
+
deviceDate: Date;
|
|
29
|
+
finalRtcw: any;
|
|
30
|
+
};
|