enterprisecargo / src /validators /telemetryValidator.js
vish85521's picture
Upload 64 files
eeb3436 verified
Raw
History Blame Contribute Delete
4.07 kB
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function toNullableNumber(value) {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function toNullableBoolean(value) {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value === "boolean") {
return value;
}
if (typeof value === "number") {
if (value === 1) {
return true;
}
if (value === 0) {
return false;
}
}
if (typeof value === "string") {
const lower = value.trim().toLowerCase();
if (["true", "1", "yes", "on"].includes(lower)) {
return true;
}
if (["false", "0", "no", "off"].includes(lower)) {
return false;
}
}
return null;
}
function normalizeTimestamp(payload) {
const raw =
payload.sourceTs ||
payload.timestamp ||
payload.ts ||
payload.time ||
payload.occurredAt ||
null;
if (raw === null) {
return new Date();
}
const date = new Date(raw);
if (!Number.isNaN(date.getTime())) {
return date;
}
const asNumber = Number(raw);
if (Number.isFinite(asNumber)) {
const millis = asNumber > 1e12 ? asNumber : asNumber * 1000;
const fromMillis = new Date(millis);
if (!Number.isNaN(fromMillis.getTime())) {
return fromMillis;
}
}
return new Date();
}
function validateAndNormalizeTelemetry(topicInfo, payload) {
const errors = [];
if (!isObject(payload)) {
return {
valid: false,
errors: ["Payload must be a JSON object"],
normalized: null,
};
}
const env = isObject(payload.env) ? payload.env : {};
const gas = isObject(payload.gas) ? payload.gas : {};
const motion = isObject(payload.motion) ? payload.motion : {};
const gpsCandidate = isObject(payload.gps)
? payload.gps
: isObject(payload.location)
? payload.location
: {};
const statusCandidate = isObject(payload.status)
? payload.status
: isObject(payload.system)
? payload.system
: {};
const temperatureC = toNullableNumber(env.temperatureC);
const gasRaw = toNullableNumber(gas.mq2Raw);
const shock = toNullableBoolean(motion.shock);
if (temperatureC === null) {
errors.push("env.temperatureC must be a number");
}
if (gasRaw === null) {
errors.push("gas.mq2Raw must be a number");
}
if (shock === null) {
errors.push("motion.shock must be a boolean");
}
if (payload.truckId && String(payload.truckId) !== String(topicInfo.truckCode)) {
errors.push("Payload truckId does not match topic truck segment");
}
if (
payload.containerId &&
String(payload.containerId) !== String(topicInfo.containerCode)
) {
errors.push("Payload containerId does not match topic container segment");
}
const normalized = {
seq: Math.max(0, Math.trunc(toNullableNumber(payload.seq) || 0)),
sourceTs: normalizeTimestamp(payload).toISOString(),
gpsLat: toNullableNumber(gpsCandidate.lat),
gpsLon: toNullableNumber(gpsCandidate.lon),
speedKph: toNullableNumber(gpsCandidate.speedKph),
temperatureC,
humidityPct: toNullableNumber(env.humidityPct),
pressureHpa: toNullableNumber(env.pressureHpa),
tiltDeg: toNullableNumber(motion.tiltDeg),
shock: Boolean(shock),
gasRaw,
gasAlert: Boolean(toNullableBoolean(gas.alert)),
sdOk: toNullableBoolean(statusCandidate.sdOk),
gpsFix:
toNullableBoolean(statusCandidate.gpsFix) ??
toNullableBoolean(gpsCandidate.gpsFix) ??
toNullableBoolean(payload.gpsFix),
uplink: statusCandidate.uplink ? String(statusCandidate.uplink) : "unknown",
rawPayload: payload,
};
return {
valid: errors.length === 0,
errors,
normalized,
};
}
module.exports = {
validateAndNormalizeTelemetry,
};