fix: wrap inbox merge in transaction, revert seed to random UUID (#60)

Address review feedback:
1. Wrap inbox deduplication (select, reassign tasks, soft-delete) in a
   db.transaction() for atomicity.
2. Revert seed.ts to use random UUID — a fixed ID shared across users
   would cause PK conflicts. The sync endpoint handles deduplication.
3. Subtasks share the same listId as their parent, so the reassign
   query already covers them (clarified in comment).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-04-08 20:54:55 -04:00
parent d9daf9eda4
commit 6c36ebcce5
2 changed files with 38 additions and 28 deletions

View file

@ -202,32 +202,44 @@ async function processOperation(op: SyncOperation, userId: string) {
// If the incoming list is an inbox, check for an existing inbox and merge
if (incomingIsInbox) {
const [existingInbox] = await db
.select()
.from(slLists)
.where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true), isNull(slLists.deletedAt)));
await db.transaction(async (tx) => {
const [existingInbox] = await tx
.select()
.from(slLists)
.where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true), isNull(slLists.deletedAt)));
if (existingInbox && existingInbox.id !== entityId) {
// Reassign all tasks from the old inbox to the new one
await db.update(slTasks)
.set({ listId: entityId, updatedAt: now })
.where(and(eq(slTasks.listId, existingInbox.id), eq(slTasks.userId, userId)));
// Soft-delete the old inbox
await db.update(slLists)
.set({ deletedAt: now, updatedAt: now })
.where(eq(slLists.id, existingInbox.id));
}
if (existingInbox && existingInbox.id !== entityId) {
// Reassign all tasks (including subtasks) from the old inbox to the new one
await tx.update(slTasks)
.set({ listId: entityId, updatedAt: now })
.where(and(eq(slTasks.listId, existingInbox.id), eq(slTasks.userId, userId)));
// Soft-delete the old inbox
await tx.update(slLists)
.set({ deletedAt: now, updatedAt: now })
.where(eq(slLists.id, existingInbox.id));
}
await tx.insert(slLists).values({
id: entityId,
userId,
name: d.name as string || 'Untitled',
color: d.color as string | undefined,
icon: d.icon as string | undefined,
position: d.position as number | undefined,
isInbox: incomingIsInbox,
}).onConflictDoNothing();
});
} else {
await db.insert(slLists).values({
id: entityId,
userId,
name: d.name as string || 'Untitled',
color: d.color as string | undefined,
icon: d.icon as string | undefined,
position: d.position as number | undefined,
isInbox: incomingIsInbox,
}).onConflictDoNothing();
}
await db.insert(slLists).values({
id: entityId,
userId,
name: d.name as string || 'Untitled',
color: d.color as string | undefined,
icon: d.icon as string | undefined,
position: d.position as number | undefined,
isInbox: incomingIsInbox,
}).onConflictDoNothing();
} else if (action === 'update') {
await verifyOwnership(slLists, entityId, userId);
await db.update(slLists)

View file

@ -18,12 +18,10 @@ async function seed() {
.from(slLists)
.where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true)));
// Use the same fixed inbox ID as the mobile app to avoid duplicates during sync
const INBOX_ID = '00000000-0000-0000-0000-000000000001';
if (existing.length === 0) {
// Let the DB generate a random UUID — the sync endpoint handles
// inbox deduplication when mobile pushes its fixed-ID inbox.
await db.insert(slLists).values({
id: INBOX_ID,
userId,
name: 'Inbox',
isInbox: true,