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:
parent
d9daf9eda4
commit
6c36ebcce5
2 changed files with 38 additions and 28 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue