From ed08f2daabc53687a6fb5cbb3cd09c5cc0893859 Mon Sep 17 00:00:00 2001 From: Dum Date: Sun, 1 Mar 2026 20:34:34 +0530 Subject: [PATCH] Small updates --- .../migration.sql | 24 +++++ prisma/schema.prisma | 2 + server/routes/lists/[id].get.ts | 1 + server/routes/sessions/[sid]/index.ts | 30 ++----- server/routes/users/[id]/lists/index.patch.ts | 2 + server/routes/users/[id]/lists/index.post.ts | 88 +++++++++++-------- .../[id]/watch-history/[tmdbid]/index.ts | 19 +--- server/utils/auth.ts | 20 +++-- 8 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 prisma/migrations/20260301145729_add_unique_constraints/migration.sql diff --git a/prisma/migrations/20260301145729_add_unique_constraints/migration.sql b/prisma/migrations/20260301145729_add_unique_constraints/migration.sql new file mode 100644 index 0000000..a57ea5c --- /dev/null +++ b/prisma/migrations/20260301145729_add_unique_constraints/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - A unique constraint covering the columns `[user_id,name]` on the table `lists` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[user,device]` on the table `sessions` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE INDEX "bookmarks_user_id_idx" ON "bookmarks" USING HASH ("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "lists_user_id_name_unique" ON "lists"("user_id", "name"); + +-- CreateIndex +CREATE INDEX "progress_items_user_id_idx" ON "progress_items" USING HASH ("user_id"); + +-- CreateIndex +CREATE INDEX "sessions_user_idx" ON "sessions" USING HASH ("user"); + +-- CreateIndex +CREATE UNIQUE INDEX "sessions_user_device_unique" ON "sessions"("user", "device"); + +-- CreateIndex +CREATE INDEX "watch_history_user_id_watched_at_idx" ON "watch_history"("user_id", "watched_at" DESC); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4b21d23..dd9b103 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -51,6 +51,7 @@ model lists { public Boolean @default(false) list_items list_items[] + @@unique([user_id, name], map: "lists_user_id_name_unique") @@index([user_id], map: "lists_user_id_index") } @@ -86,6 +87,7 @@ model sessions { device String user_agent String + @@unique([user, device], map: "sessions_user_device_unique") @@index([user], type: Hash) } diff --git a/server/routes/lists/[id].get.ts b/server/routes/lists/[id].get.ts index 6056d1e..e5ea202 100644 --- a/server/routes/lists/[id].get.ts +++ b/server/routes/lists/[id].get.ts @@ -3,6 +3,7 @@ import { prisma } from '#imports'; export default defineEventHandler(async event => { const id = event.context.params?.id; const listInfo = await prisma.lists.findUnique({ + relationLoadStrategy: 'join', where: { id: id, }, diff --git a/server/routes/sessions/[sid]/index.ts b/server/routes/sessions/[sid]/index.ts index 48b73ba..4ecddaa 100644 --- a/server/routes/sessions/[sid]/index.ts +++ b/server/routes/sessions/[sid]/index.ts @@ -39,18 +39,13 @@ export default defineEventHandler(async event => { const body = await readBody(event); const validatedBody = updateSessionSchema.parse(body); - if (validatedBody.deviceName) { - await prisma.sessions.update({ - where: { id: sessionId }, - data: { - device: validatedBody.deviceName, - }, - }); - } - - const updatedSession = await prisma.sessions.findUnique({ - where: { id: sessionId }, - }); + // Use update return value directly — no redundant findUnique + const updatedSession = validatedBody.deviceName + ? await prisma.sessions.update({ + where: { id: sessionId }, + data: { device: validatedBody.deviceName }, + }) + : targetedSession; return { id: updatedSession.id, @@ -65,16 +60,7 @@ export default defineEventHandler(async event => { } if (event.method === 'DELETE') { - const sid = event.context.params?.sid; - const sessionExists = await prisma.sessions.findUnique({ - where: { id: sid }, - }); - - if (!sessionExists) { - return { success: true }; - } - const session = await useAuth().getSessionAndBump(sid); - + // targetedSession already validated above — no redundant findUnique or session bump needed await prisma.sessions.delete({ where: { id: sessionId }, }); diff --git a/server/routes/users/[id]/lists/index.patch.ts b/server/routes/users/[id]/lists/index.patch.ts index c6f9e36..87a1fd6 100644 --- a/server/routes/users/[id]/lists/index.patch.ts +++ b/server/routes/users/[id]/lists/index.patch.ts @@ -32,6 +32,7 @@ export default defineEventHandler(async event => { const validatedBody = updateListSchema.parse(body); const list = await prisma.lists.findUnique({ + relationLoadStrategy: 'join', where: { id: validatedBody.list_id }, include: { list_items: true }, }); @@ -100,6 +101,7 @@ export default defineEventHandler(async event => { } return tx.lists.findUnique({ + relationLoadStrategy: 'join', where: { id: list.id }, include: { list_items: true }, }); diff --git a/server/routes/users/[id]/lists/index.post.ts b/server/routes/users/[id]/lists/index.post.ts index aa8261f..51bebe3 100644 --- a/server/routes/users/[id]/lists/index.post.ts +++ b/server/routes/users/[id]/lists/index.post.ts @@ -40,47 +40,57 @@ export default defineEventHandler(async event => { const validatedBody = createListSchema.parse(parsedBody); - const result = await prisma.$transaction(async tx => { - const existing = await tx.lists.findFirst({ - where: { user_id: userId, name: validatedBody.name } + try { + const result = await prisma.$transaction(async tx => { + // App-level guard for a clean 409 message + const existing = await tx.lists.findFirst({ + where: { user_id: userId, name: validatedBody.name }, + }); + + if (existing) { + throw createError({ statusCode: 409, message: 'A list with this name already exists' }); + } + + const now = new Date(); + const newList = await tx.lists.create({ + data: { + id: uuidv7(), + user_id: userId, + name: validatedBody.name, + description: validatedBody.description || null, + public: validatedBody.public || false, + updated_at: now, + }, + }); + + if (validatedBody.items && validatedBody.items.length > 0) { + await tx.list_items.createMany({ + data: validatedBody.items.map(item => ({ + id: uuidv7(), + list_id: newList.id, + tmdb_id: item.tmdb_id, + type: item.type, + })), + skipDuplicates: true, + }); + } + + return tx.lists.findUnique({ + relationLoadStrategy: 'join', + where: { id: newList.id }, + include: { list_items: true }, + }); }); - if (existing) { + return { + list: result, + message: 'List created successfully', + }; + } catch (err: any) { + // DB-level safety net: catch unique constraint violation from @@unique([user_id, name]) + if (err.code === 'P2002') { throw createError({ statusCode: 409, message: 'A list with this name already exists' }); } - - const now = new Date(); - const newList = await tx.lists.create({ - data: { - id: uuidv7(), - user_id: userId, - name: validatedBody.name, - description: validatedBody.description || null, - public: validatedBody.public || false, - updated_at: now, - }, - }); - - if (validatedBody.items && validatedBody.items.length > 0) { - await tx.list_items.createMany({ - data: validatedBody.items.map(item => ({ - id: uuidv7(), - list_id: newList.id, - tmdb_id: item.tmdb_id, - type: item.type, // Type is mapped here - })), - skipDuplicates: true, - }); - } - - return tx.lists.findUnique({ - where: { id: newList.id }, - include: { list_items: true }, - }); - }); - - return { - list: result, - message: 'List created successfully', - }; + throw err; + } }); diff --git a/server/routes/users/[id]/watch-history/[tmdbid]/index.ts b/server/routes/users/[id]/watch-history/[tmdbid]/index.ts index 89e24cd..d9e1663 100644 --- a/server/routes/users/[id]/watch-history/[tmdbid]/index.ts +++ b/server/routes/users/[id]/watch-history/[tmdbid]/index.ts @@ -144,27 +144,14 @@ export default defineEventHandler(async event => { if (body.seasonId) whereClause.season_id = body.seasonId; if (body.episodeId) whereClause.episode_id = body.episodeId; - const itemsToDelete = await prisma.watch_history.findMany({ - where: whereClause, - }); - - if (itemsToDelete.length === 0) { - return { - success: true, - count: 0, - tmdbId, - episodeId: body.episodeId, - seasonId: body.seasonId, - }; - } - - await prisma.watch_history.deleteMany({ + // Use deleteMany return count directly — no redundant findMany + const { count } = await prisma.watch_history.deleteMany({ where: whereClause, }); return { success: true, - count: itemsToDelete.length, + count, tmdbId, episodeId: body.episodeId, seasonId: body.seasonId, diff --git a/server/utils/auth.ts b/server/utils/auth.ts index 8033f22..0025d39 100644 --- a/server/utils/auth.ts +++ b/server/utils/auth.ts @@ -40,13 +40,19 @@ export function useAuth() { const now = new Date(); const expiryDate = new Date(now.getTime() + SESSION_EXPIRY_MS); - // Cleanup existing session for this device - await prisma.sessions.deleteMany({ - where: { user, device }, - }); - - return await prisma.sessions.create({ - data: { + // Atomic upsert — backed by @@unique([user, device]) in schema + return await prisma.sessions.upsert({ + where: { + sessions_user_device_unique: { user, device }, + }, + update: { + id: uuidv7(), + user_agent: userAgent, + created_at: now, + accessed_at: now, + expires_at: expiryDate, + }, + create: { id: uuidv7(), user, device,