144 lines
4.2 KiB
JavaScript
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) ?? [] }));
|
|
}
|