This repository has been archived on 2026-10-08. You can view files and clone it, but cannot push or open issues or pull requests.

144 lines
4.2 KiB
JavaScript

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<Map<string, string[]>>} 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) ?? [] }));
}