fix: prevent data loss in routes, fragments, and group operations

Found and fixed multiple critical bugs with similar patterns:

1. routeStatements() used DELETE FROM d1_routes before re-inserting
   - Could cause data loss if interrupted or if logic changes
   - Now uses DELETE with WHERE clause + INSERT...ON CONFLICT (upsert)

2. saveFragments() used DELETE FROM d1_fragments before re-inserting
   - Same pattern as routes, fixed with targeted deletes + upsert
   - Added loadFragments() call to determine what to delete

3. adminGroupRename() didn't update fragment groupId
   - When renaming a group, fragments were left pointing to old groupId
   - Now updates fragments alongside routes, secrets, and invites

All changes follow the same safe pattern:
- Load existing data to identify what needs deletion
- Delete only removed items with WHERE clauses
- Use INSERT...ON CONFLICT DO UPDATE for upserts
- Never use bare DELETE FROM table

Testing: All 339 tests pass.
This commit is contained in:
RhenCloud 2026-09-10 22:35:06 +08:00
parent 6c6c67fd73
commit 1a0500b812
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
3 changed files with 75 additions and 14 deletions

View file

@ -35,16 +35,39 @@ export async function loadFragments(db: D1Database): Promise<NamedFragment[]> {
export async function saveFragments(db: D1Database, fragments: NamedFragment[]): Promise<void> {
const now = Date.now();
const statements: D1PreparedStatement[] = [
db.prepare("DELETE FROM d1_fragments"),
...fragments.map((f) =>
// Load existing fragments to determine what to delete
const existing = await loadFragments(db);
const existingKeys = new Set(existing.map((f) => `${f.id}:${f.groupId ?? ""}`));
const newKeys = new Set(fragments.map((f) => `${f.id}:${f.groupId ?? ""}`));
const statements: D1PreparedStatement[] = [];
// Delete fragments that are no longer present
for (const key of existingKeys) {
if (!newKeys.has(key)) {
const [id, groupId] = key.split(":");
statements.push(
db.prepare("DELETE FROM d1_fragments WHERE id = ? AND group_id = ?").bind(id, groupId)
);
}
}
// Upsert all fragments
for (const f of fragments) {
statements.push(
db
.prepare(
`INSERT INTO d1_fragments (id, group_id, name, node, version, created_at, updated_at)
VALUES (?, ?, ?, ?, 1, ?, ?)`,
VALUES (?, ?, ?, ?, 1, ?, ?)
ON CONFLICT(id, group_id) DO UPDATE SET
name = excluded.name,
node = excluded.node,
updated_at = excluded.updated_at`,
)
.bind(f.id, f.groupId ?? "", f.name, JSON.stringify(f.node), now, now),
),
];
);
}
await db.batch(statements);
}