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

@ -965,7 +965,7 @@ export async function adminGroupRename(
return respondError(event, 500, "Failed to save groups");
}
// Re-point routes, the tenant webhook secret and pending invites.
// Re-point routes, fragments, the tenant webhook secret and pending invites.
const routes = await loadRoutes(env.KV);
const touched = routes.filter((r) => r.groupId === groupId);
if (touched.length > 0) {
@ -974,6 +974,14 @@ export async function adminGroupRename(
routes.map((r) => (r.groupId === groupId ? { ...r, groupId: newId } : r)),
);
}
const fragments = await loadFragments(env.DB);
const touchedFragments = fragments.filter((f) => f.groupId === groupId);
if (touchedFragments.length > 0) {
await saveFragments(
env.DB,
fragments.map((f) => (f.groupId === groupId ? { ...f, groupId: newId } : f)),
);
}
const secret = await getTenantSecret(env.KV, groupId);
if (secret) {
await env.KV.put(`tenant:${newId}`, secret);