fix colliding drawing IDs

This commit is contained in:
Zimeng Xiong
2026-02-07 12:08:49 -08:00
parent 8fcca43b0d
commit c40a5f46a0
5 changed files with 178 additions and 14 deletions
+35 -5
View File
@@ -334,7 +334,20 @@ export const registerDashboardRoutes = (
}
}
const updatedDrawing = await prisma.drawing.update({ where: { id }, data });
const updateResult = await prisma.drawing.updateMany({
where: { id, userId: req.user.id },
data,
});
if (updateResult.count === 0) {
return res.status(404).json({ error: "Drawing not found" });
}
const updatedDrawing = await prisma.drawing.findFirst({
where: { id, userId: req.user.id },
});
if (!updatedDrawing) {
return res.status(404).json({ error: "Drawing not found" });
}
invalidateDrawingsCache();
return res.json({
@@ -352,7 +365,12 @@ export const registerDashboardRoutes = (
const drawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
if (!drawing) return res.status(404).json({ error: "Drawing not found" });
await prisma.drawing.delete({ where: { id } });
const deleteResult = await prisma.drawing.deleteMany({
where: { id, userId: req.user.id },
});
if (deleteResult.count === 0) {
return res.status(404).json({ error: "Drawing not found" });
}
invalidateDrawingsCache();
if (config.enableAuditLogging) {
@@ -375,6 +393,9 @@ export const registerDashboardRoutes = (
const { id } = req.params;
const original = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
if (!original) return res.status(404).json({ error: "Original drawing not found" });
if (original.collectionId === "trash") {
await ensureTrashCollection(prisma, req.user.id);
}
const newDrawing = await prisma.drawing.create({
data: {
@@ -443,10 +464,19 @@ export const registerDashboardRoutes = (
}
const sanitizedName = sanitizeText(parsed.data, 100);
const updatedCollection = await prisma.collection.update({
where: { id },
const updateResult = await prisma.collection.updateMany({
where: { id, userId: req.user.id },
data: { name: sanitizedName },
});
if (updateResult.count === 0) {
return res.status(404).json({ error: "Collection not found" });
}
const updatedCollection = await prisma.collection.findFirst({
where: { id, userId: req.user.id },
});
if (!updatedCollection) {
return res.status(404).json({ error: "Collection not found" });
}
return res.json(updatedCollection);
}));
@@ -464,7 +494,7 @@ export const registerDashboardRoutes = (
where: { collectionId: id, userId: req.user.id },
data: { collectionId: null },
}),
prisma.collection.delete({ where: { id } }),
prisma.collection.deleteMany({ where: { id, userId: req.user.id } }),
]);
invalidateDrawingsCache();
+106
View File
@@ -131,6 +131,21 @@ const makeUniqueName = (base: string, used: Set<string>): string => {
return candidate;
};
const findFirstDuplicate = (values: string[]): string | null => {
const seen = new Set<string>();
for (const value of values) {
if (seen.has(value)) return value;
seen.add(value);
}
return null;
};
const normalizeNonEmptyId = (value: unknown): string | null => {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const findSqliteTable = (tables: string[], candidates: string[]): string | null => {
const byLower = new Map(tables.map((t) => [t.toLowerCase(), t]));
for (const candidate of candidates) {
@@ -439,6 +454,28 @@ Drawings: ${drawings.length}
message: `Too many drawings (max ${MAX_IMPORT_DRAWINGS})`,
});
}
const duplicateCollectionId = findFirstDuplicate(manifest.collections.map((c) => c.id));
if (duplicateCollectionId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate collection id in manifest: ${duplicateCollectionId}`,
});
}
const duplicateDrawingId = findFirstDuplicate(manifest.drawings.map((d) => d.id));
if (duplicateDrawingId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing id in manifest: ${duplicateDrawingId}`,
});
}
const duplicateDrawingPath = findFirstDuplicate(manifest.drawings.map((d) => d.filePath));
if (duplicateDrawingPath) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing file path in manifest: ${duplicateDrawingPath}`,
});
}
for (const drawing of manifest.drawings) {
if (!getSafeZipEntry(zip, drawing.filePath)) {
return res.status(400).json({
@@ -532,6 +569,28 @@ Drawings: ${drawings.length}
});
}
const duplicateCollectionId = findFirstDuplicate(manifest.collections.map((c) => c.id));
if (duplicateCollectionId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate collection id in manifest: ${duplicateCollectionId}`,
});
}
const duplicateDrawingId = findFirstDuplicate(manifest.drawings.map((d) => d.id));
if (duplicateDrawingId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing id in manifest: ${duplicateDrawingId}`,
});
}
const duplicateDrawingPath = findFirstDuplicate(manifest.drawings.map((d) => d.filePath));
if (duplicateDrawingPath) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing file path in manifest: ${duplicateDrawingPath}`,
});
}
type PreparedImportDrawing = {
id: string;
name: string;
@@ -772,6 +831,31 @@ Drawings: ${drawings.length}
});
}
const duplicateDrawingIdRow = db
.prepare(
`SELECT id FROM "${drawingTable}" WHERE id IS NOT NULL GROUP BY id HAVING COUNT(1) > 1 LIMIT 1`
)
.get();
if (duplicateDrawingIdRow?.id) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate drawing id in legacy DB: ${String(duplicateDrawingIdRow.id)}`,
});
}
if (collectionTable) {
const duplicateCollectionIdRow = db
.prepare(
`SELECT id FROM "${collectionTable}" WHERE id IS NOT NULL GROUP BY id HAVING COUNT(1) > 1 LIMIT 1`
)
.get();
if (duplicateCollectionIdRow?.id) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate collection id in legacy DB: ${String(duplicateCollectionIdRow.id)}`,
});
}
}
let latestMigration: string | null = null;
const migrationsTable = findSqliteTable(tables, ["_prisma_migrations"]);
if (migrationsTable) {
@@ -862,6 +946,28 @@ Drawings: ${drawings.length}
});
}
const importedCollectionIds = importedCollections
.map((c) => normalizeNonEmptyId(c?.id))
.filter((id): id is string => id !== null);
const duplicateCollectionId = findFirstDuplicate(importedCollectionIds);
if (duplicateCollectionId) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate collection id in legacy DB: ${duplicateCollectionId}`,
});
}
const importedDrawingIds = importedDrawings
.map((d) => normalizeNonEmptyId(d?.id))
.filter((id): id is string => id !== null);
const duplicateDrawingId = findFirstDuplicate(importedDrawingIds);
if (duplicateDrawingId) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate drawing id in legacy DB: ${duplicateDrawingId}`,
});
}
type PreparedLegacyDrawing = {
importedId: string | null;
name: string;