From ad7d349c85e381f035a5ceeaae086380f4c727d1 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sun, 26 Jul 2026 14:54:38 +0200 Subject: [PATCH] fix(todo): wrap category-assignee replacement in a transaction updateCategoryAssignees ran its DELETE + INSERT OR IGNORE loop unwrapped, so a mid-loop failure could leave a category half-cleared. Also widens the item params to the nullable shapes the wire contract actually sends (prep for the todo body validation). --- server/src/nest/todo/todo.service.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/server/src/nest/todo/todo.service.ts b/server/src/nest/todo/todo.service.ts index 96a96fea1..05357a762 100644 --- a/server/src/nest/todo/todo.service.ts +++ b/server/src/nest/todo/todo.service.ts @@ -40,7 +40,7 @@ export class TodoService { } createItem(tripId: string | number, data: { - name: string; category?: string; due_date?: string; description?: string; assigned_user_id?: number; priority?: number; + name: string; category?: string | null; due_date?: string | null; description?: string | null; assigned_user_id?: number | null; priority?: number; }) { const maxOrder = this.db.get<{ max: number | null }>('SELECT MAX(sort_order) as max FROM todo_items WHERE trip_id = ?', tripId)!; const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1; @@ -57,7 +57,7 @@ export class TodoService { updateItem( tripId: string | number, id: string | number, - data: { name?: string; checked?: number; category?: string; due_date?: string | null; description?: string | null; assigned_user_id?: number | null; priority?: number | null }, + data: { name?: string; checked?: number; category?: string | null; due_date?: string | null; description?: string | null; assigned_user_id?: number | null; priority?: number | null }, bodyKeys: string[] ) { const item = this.db.get('SELECT * FROM todo_items WHERE id = ? AND trip_id = ?', id, tripId); @@ -127,12 +127,14 @@ export class TodoService { } updateCategoryAssignees(tripId: string | number, categoryName: string, userIds: number[] | undefined) { - this.db.run('DELETE FROM todo_category_assignees WHERE trip_id = ? AND category_name = ?', tripId, categoryName); + this.db.transaction(() => { + this.db.run('DELETE FROM todo_category_assignees WHERE trip_id = ? AND category_name = ?', tripId, categoryName); - if (Array.isArray(userIds) && userIds.length > 0) { - const insert = this.db.prepare('INSERT OR IGNORE INTO todo_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)'); - for (const uid of userIds) insert.run(tripId, categoryName, uid); - } + if (Array.isArray(userIds) && userIds.length > 0) { + const insert = this.db.prepare('INSERT OR IGNORE INTO todo_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)'); + for (const uid of userIds) insert.run(tripId, categoryName, uid); + } + }); return this.db.all(` SELECT tca.user_id, u.username, u.avatar