diff --git a/artifacts/api-server/src/routes/services.ts b/artifacts/api-server/src/routes/services.ts index a3ded34a..fd62ca32 100644 --- a/artifacts/api-server/src/routes/services.ts +++ b/artifacts/api-server/src/routes/services.ts @@ -87,34 +87,48 @@ router.patch("/services/reorder", requireAdmin, async (req, res): Promise } const { orderedIds } = parsed.data; + // Validate the provided ids are unique and all reference real + // services. We intentionally do NOT require the client to send + // every service id — the iPad UI may filter or hide rows, and + // forcing a full snapshot caused false 400s ("orderedIds must + // contain every service id exactly once"). Any services that + // weren't included are pushed to the end of the list so they + // remain visible without colliding on sortOrder. const existing = await db .select({ id: servicesTable.id }) .from(servicesTable); const existingIds = new Set(existing.map((s) => s.id)); - if (orderedIds.length !== existingIds.size) { - res.status(400).json({ - error: "orderedIds must contain every service id exactly once", - }); - return; - } const seen = new Set(); for (const id of orderedIds) { if (!existingIds.has(id) || seen.has(id)) { res.status(400).json({ - error: "orderedIds must contain every service id exactly once", + error: "orderedIds contains a duplicate or unknown service id", }); return; } seen.add(id); } + const trailingIds = existing + .map((s) => s.id) + .filter((id) => !seen.has(id)); + await db.transaction(async (tx) => { - for (let i = 0; i < orderedIds.length; i++) { + let index = 0; + for (const id of orderedIds) { await tx .update(servicesTable) - .set({ sortOrder: i }) - .where(eq(servicesTable.id, orderedIds[i])); + .set({ sortOrder: index }) + .where(eq(servicesTable.id, id)); + index += 1; + } + for (const id of trailingIds) { + await tx + .update(servicesTable) + .set({ sortOrder: index }) + .where(eq(servicesTable.id, id)); + index += 1; } });