import { Node, Patch } from "../db/models/index.js"; import { getConfiguredDevice } from "./devices.js"; const SERIAL_RE = /^[0-9A-F]{8}$/; export class NodeInputError extends Error {} /** @returns {string} upper-case serial, or throws NodeInputError */ export function normalizeSerial(value) { const s = String(value ?? "").trim().toUpperCase(); if (!SERIAL_RE.test(s)) { throw new NodeInputError(`Invalid node serial "${value}"`); } return s; } function normalizeSerialList(values) { if (!Array.isArray(values)) { throw new NodeInputError("Expected a list of node serials"); } return [...new Set(values.map(normalizeSerial))]; } export function formatClientNode(n) { return { serial: n.serial, patches: Array.isArray(n.patches) ? n.patches : [], updatedAt: n.updatedAt, }; } async function requireDevice() { const device = await getConfiguredDevice(); if (!device) throw new Error("No device configured"); return device; } /** Validates patch ids against the configured device's patches. */ async function normalizePatchIds(deviceId, values) { if (!Array.isArray(values)) { throw new NodeInputError("Expected a list of patch ids"); } const ids = [ ...new Set(values.map((v) => String(v ?? "").trim()).filter(Boolean)), ]; if (ids.length === 0) return ids; const found = await Patch.find({ deviceId, patchId: { $in: ids } }) .select("patchId") .lean(); const known = new Set(found.map((p) => p.patchId)); const unknown = ids.filter((id) => !known.has(id)); if (unknown.length) { throw new NodeInputError(`Unknown patch id: ${unknown.join(", ")}`); } return ids; } export async function listNodes() { const device = await getConfiguredDevice(); if (!device) return []; const nodes = await Node.find({ deviceId: device._id }) .sort({ serial: 1 }) .lean(); return nodes.map(formatClientNode); } /** Creates Node documents for serials that do not exist yet. */ export async function upsertNodes(serials) { const device = await requireDevice(); const list = normalizeSerialList(serials); for (const serial of list) { await Node.updateOne( { deviceId: device._id, serial }, { $setOnInsert: { deviceId: device._id, serial, patches: [] } }, { upsert: true }, ); } return listNodes(); } /** Replaces one node's patch list, creating the node if needed. */ export async function setNodePatches(serial, patchIds) { const device = await requireDevice(); const s = normalizeSerial(serial); const patches = await normalizePatchIds(device._id, patchIds); const node = await Node.findOneAndUpdate( { deviceId: device._id, serial: s }, { $set: { patches }, $setOnInsert: { deviceId: device._id, serial: s } }, { new: true, upsert: true }, ).lean(); return formatClientNode(node); } /** * Makes `serials` the exact set of nodes assigned to `patchId`: adds the patch * to each selected node (creating it if needed) and removes it from the rest. */ export async function setPatchNodes(patchId, serials) { const device = await requireDevice(); const id = String(patchId ?? "").trim(); if (!id) throw new NodeInputError("patchId required"); const list = normalizeSerialList(serials); await Node.updateMany( { deviceId: device._id, serial: { $nin: list }, patches: id }, { $pull: { patches: id } }, ); for (const serial of list) { await Node.updateOne( { deviceId: device._id, serial }, { $addToSet: { patches: id }, $setOnInsert: { deviceId: device._id, serial }, }, { upsert: true }, ); } return list; } /** @returns {Promise>} patchId -> sorted node serials */ export async function getNodesByPatch() { const nodes = await listNodes(); const map = new Map(); for (const n of nodes) { for (const patchId of n.patches) { if (!map.has(patchId)) map.set(patchId, []); map.get(patchId).push(n.serial); } } return map; } export async function nodesForPatch(patchId) { const map = await getNodesByPatch(); return map.get(patchId) ?? []; } /** Adds `nodes: string[]` to client patch objects. */ export async function attachNodesToPatches(patches) { const map = await getNodesByPatch(); return patches.map((p) => ({ ...p, nodes: map.get(p.id) ?? [] })); }