From b99d88cf2e41d63beafea421d61f88287675e59b Mon Sep 17 00:00:00 2001 From: FifthWit Date: Wed, 2 Apr 2025 21:04:46 -0500 Subject: [PATCH] Added custom lists --- .../20250403013111_added_lists/migration.sql | 30 +++++ prisma/schema.prisma | 22 ++++ .../users/[id]/lists/[listId].delete.ts | 49 ++++++++ server/routes/users/[id]/lists/index.get.ts | 29 +++++ server/routes/users/[id]/lists/index.patch.ts | 108 ++++++++++++++++++ server/routes/users/[id]/lists/index.post.ts | 72 ++++++++++++ 6 files changed, 310 insertions(+) create mode 100644 prisma/migrations/20250403013111_added_lists/migration.sql create mode 100644 server/routes/users/[id]/lists/[listId].delete.ts create mode 100644 server/routes/users/[id]/lists/index.get.ts create mode 100644 server/routes/users/[id]/lists/index.patch.ts create mode 100644 server/routes/users/[id]/lists/index.post.ts diff --git a/prisma/migrations/20250403013111_added_lists/migration.sql b/prisma/migrations/20250403013111_added_lists/migration.sql new file mode 100644 index 0000000..5b935cd --- /dev/null +++ b/prisma/migrations/20250403013111_added_lists/migration.sql @@ -0,0 +1,30 @@ +-- CreateTable +CREATE TABLE "lists" ( + "id" UUID NOT NULL, + "user_id" VARCHAR(255) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "description" VARCHAR(255), + "created_at" TIMESTAMPTZ(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(0) NOT NULL, + + CONSTRAINT "lists_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "list_items" ( + "id" UUID NOT NULL, + "list_id" UUID NOT NULL, + "tmdb_id" VARCHAR(255) NOT NULL, + "added_at" TIMESTAMPTZ(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "list_items_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "lists_user_id_index" ON "lists"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "list_items_list_id_tmdb_id_unique" ON "list_items"("list_id", "tmdb_id"); + +-- AddForeignKey +ALTER TABLE "list_items" ADD CONSTRAINT "list_items_list_id_fkey" FOREIGN KEY ("list_id") REFERENCES "lists"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index caa1495..8b2e2d0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -76,4 +76,26 @@ model users { permissions String[] ratings Json @default("[]") profile Json +} + +model lists { + id String @id @db.Uuid @default(uuid()) + user_id String @db.VarChar(255) + name String @db.VarChar(255) + description String? @db.VarChar(255) + created_at DateTime @default(now()) @db.Timestamptz(0) + updated_at DateTime @updatedAt @db.Timestamptz(0) + list_items list_items[] + + @@index([user_id], map: "lists_user_id_index") +} + +model list_items { + id String @id @db.Uuid @default(uuid()) + list_id String @db.Uuid + tmdb_id String @db.VarChar(255) + added_at DateTime @default(now()) @db.Timestamptz(0) + list lists @relation(fields: [list_id], references: [id]) + + @@unique([list_id, tmdb_id], map: "list_items_list_id_tmdb_id_unique") } \ No newline at end of file diff --git a/server/routes/users/[id]/lists/[listId].delete.ts b/server/routes/users/[id]/lists/[listId].delete.ts new file mode 100644 index 0000000..30f6170 --- /dev/null +++ b/server/routes/users/[id]/lists/[listId].delete.ts @@ -0,0 +1,49 @@ +import { useAuth } from "#imports"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + const userId = event.context.params?.id; + const listId = event.context.params?.listId; + const session = await useAuth().getCurrentSession(); + + if (session.user !== userId) { + throw createError({ + statusCode: 403, + message: "Cannot delete lists for other users", + }); + } + const list = await prisma.lists.findUnique({ + where: { id: listId }, + }); + + if (!list) { + throw createError({ + statusCode: 404, + message: "List not found", + }); + } + + if (list.user_id !== userId) { + throw createError({ + statusCode: 403, + message: "Cannot delete lists you don't own", + }); + } + + await prisma.$transaction(async (tx) => { + await tx.list_items.deleteMany({ + where: { list_id: listId }, + }); + + await tx.lists.delete({ + where: { id: listId }, + }); + }); + + return { + id: listId, + message: "List deleted successfully", + }; +}); diff --git a/server/routes/users/[id]/lists/index.get.ts b/server/routes/users/[id]/lists/index.get.ts new file mode 100644 index 0000000..e5b8437 --- /dev/null +++ b/server/routes/users/[id]/lists/index.get.ts @@ -0,0 +1,29 @@ +import { useAuth } from "#imports"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + const userId = event.context.params?.id; + const session = await useAuth().getCurrentSession(); + + if (session.user !== userId) { + throw createError({ + statusCode: 403, + message: "Cannot access other user information", + }); + } + + const lists = await prisma.lists.findMany({ + where: { + user_id: userId, + }, + include: { + list_items: true, + }, + }); + + return { + lists, + }; +}); diff --git a/server/routes/users/[id]/lists/index.patch.ts b/server/routes/users/[id]/lists/index.patch.ts new file mode 100644 index 0000000..e5c9407 --- /dev/null +++ b/server/routes/users/[id]/lists/index.patch.ts @@ -0,0 +1,108 @@ +import { useAuth } from "#imports"; +import { PrismaClient } from "@prisma/client"; +import { z } from "zod"; + +const prisma = new PrismaClient(); + +// Schema for validating the request body +const listItemSchema = z.object({ + tmdb_id: z.string(), +}); + +const updateListSchema = z.object({ + list_id: z.string().uuid(), + name: z.string().min(1).max(255).optional(), + description: z.string().max(255).optional().nullable(), + addItems: z.array(listItemSchema).optional(), + removeItems: z.array(listItemSchema).optional(), +}); + +export default defineEventHandler(async (event) => { + const userId = event.context.params?.id; + const session = await useAuth().getCurrentSession(); + + if (session.user !== userId) { + throw createError({ + statusCode: 403, + message: "Cannot modify lists for other users", + }); + } + + const body = await readBody(event); + const validatedBody = updateListSchema.parse(body); + + const list = await prisma.lists.findUnique({ + where: { id: validatedBody.list_id }, + include: { list_items: true }, + }); + + if (!list) { + throw createError({ + statusCode: 404, + message: "List not found", + }); + } + + if (list.user_id !== userId) { + throw createError({ + statusCode: 403, + message: "Cannot modify lists you don't own", + }); + } + + const result = await prisma.$transaction(async (tx) => { + if (validatedBody.name || validatedBody.description !== undefined) { + await tx.lists.update({ + where: { id: list.id }, + data: { + name: validatedBody.name || list.name, + description: + validatedBody.description !== undefined + ? validatedBody.description + : list.description, + }, + }); + } + + if (validatedBody.addItems && validatedBody.addItems.length > 0) { + const existingTmdbIds = list.list_items.map((item) => item.tmdb_id); + + const itemsToAdd = validatedBody.addItems.filter( + (item) => !existingTmdbIds.includes(item.tmdb_id) + ); + + if (itemsToAdd.length > 0) { + await tx.list_items.createMany({ + data: itemsToAdd.map((item) => ({ + list_id: list.id, + tmdb_id: item.tmdb_id, + })), + skipDuplicates: true, + }); + } + } + + if (validatedBody.removeItems && validatedBody.removeItems.length > 0) { + const tmdbIdsToRemove = validatedBody.removeItems.map( + (item) => item.tmdb_id + ); + + await tx.list_items.deleteMany({ + where: { + list_id: list.id, + tmdb_id: { in: tmdbIdsToRemove }, + }, + }); + } + + return tx.lists.findUnique({ + where: { id: list.id }, + include: { list_items: true }, + }); + }); + + return { + list: result, + message: "List updated successfully", + }; +}); diff --git a/server/routes/users/[id]/lists/index.post.ts b/server/routes/users/[id]/lists/index.post.ts new file mode 100644 index 0000000..a3ac1b5 --- /dev/null +++ b/server/routes/users/[id]/lists/index.post.ts @@ -0,0 +1,72 @@ +import { useAuth } from "#imports"; +import { PrismaClient } from "@prisma/client"; +import { z } from "zod"; + +const prisma = new PrismaClient(); + +// Schema for validating the request body +const listItemSchema = z.object({ + tmdb_id: z.string(), +}); + +const createListSchema = z.object({ + name: z.string().min(1).max(255), + description: z.string().max(255).optional().nullable(), + items: z.array(listItemSchema).optional(), +}); + +export default defineEventHandler(async (event) => { + const userId = event.context.params?.id; + const session = await useAuth().getCurrentSession(); + + if (session.user !== userId) { + throw createError({ + statusCode: 403, + message: "Cannot modify user other than yourself", + }); + } + + const body = await readBody(event); + + let parsedBody; + try { + parsedBody = typeof body === "string" ? JSON.parse(body) : body; + } catch (error) { + throw createError({ + statusCode: 400, + message: "Invalid request body format", + }); + } + + const validatedBody = createListSchema.parse(parsedBody); + + const result = await prisma.$transaction(async (tx) => { + const newList = await tx.lists.create({ + data: { + user_id: userId, + name: validatedBody.name, + description: validatedBody.description || null, + }, + }); + + if (validatedBody.items && validatedBody.items.length > 0) { + await tx.list_items.createMany({ + data: validatedBody.items.map((item) => ({ + list_id: newList.id, + tmdb_id: item.tmdb_id, + })), + skipDuplicates: true, + }); + } + + return tx.lists.findUnique({ + where: { id: newList.id }, + include: { list_items: true }, + }); + }); + + return { + list: result, + message: "List created successfully", + }; +});