From e798338107f3333ecc7887a06d7f9dee7f2dd02a Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Fri, 27 Mar 2026 22:56:18 +0700 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20db:=20update=20sche?= =?UTF-8?q?ma=20for=20new=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 14 ++++++++++++++ prisma/schema.prisma | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20260327155457_add_collection_unique_by_owner_and_id/migration.sql diff --git a/prisma/migrations/20260327155457_add_collection_unique_by_owner_and_id/migration.sql b/prisma/migrations/20260327155457_add_collection_unique_by_owner_and_id/migration.sql new file mode 100644 index 0000000..5a06b55 --- /dev/null +++ b/prisma/migrations/20260327155457_add_collection_unique_by_owner_and_id/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to alter the column `name` on the `collections` table. The data in that column could be lost. The data in that column will be cast from `VarChar(255)` to `VarChar(115)`. + - A unique constraint covering the columns `[slug,ownerId]` on the table `collections` will be added. If there are existing duplicate values, this will fail. + - Added the required column `slug` to the `collections` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "collections" ADD COLUMN "slug" VARCHAR(115) NOT NULL, +ALTER COLUMN "name" SET DATA TYPE VARCHAR(115); + +-- CreateIndex +CREATE UNIQUE INDEX "collections_slug_ownerId_key" ON "collections"("slug", "ownerId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f91c2c2..1c5f444 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -418,7 +418,8 @@ model UserLog { model Collection { id String @id @db.Uuid - name String @db.VarChar(255) + name String @db.VarChar(115) + slug String @db.VarChar(115) medias Media[] @relation("MediaCollections") owner User @relation("UserCollections", fields: [ownerId], references: [id]) ownerId String @db.Uuid @@ -429,6 +430,8 @@ model Collection { deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + + @@unique([slug, ownerId]) @@map("collections") } -- 2.49.0 From 56c921e8009783a8ac1f7cf85e8fdb4664a9c13d Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Fri, 27 Mar 2026 23:42:26 +0700 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=91=94=20feat:=20add=20collection=20m?= =?UTF-8?q?odule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../addItemToCollection.controller.ts | 10 ++++++ src/modules/collection/index.ts | 9 +++++ .../schemas/addItemToCollection.schema.ts | 35 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/modules/collection/controllers/addItemToCollection.controller.ts create mode 100644 src/modules/collection/index.ts create mode 100644 src/modules/collection/schemas/addItemToCollection.schema.ts diff --git a/src/modules/collection/controllers/addItemToCollection.controller.ts b/src/modules/collection/controllers/addItemToCollection.controller.ts new file mode 100644 index 0000000..5024e0e --- /dev/null +++ b/src/modules/collection/controllers/addItemToCollection.controller.ts @@ -0,0 +1,10 @@ +import { Context, Static } from "elysia"; +import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; +import { addItemToCollectionSchema } from "../schemas/addItemToCollection.schema"; + +export const addItemToCollectionController = async (ctx: { + set: Context["set"]; + headers: Static; +}) => { + return returnWriteResponse(ctx.set, 200, "Item added to collection successfully" + ctx.headers.cookie); +}; diff --git a/src/modules/collection/index.ts b/src/modules/collection/index.ts new file mode 100644 index 0000000..781d87c --- /dev/null +++ b/src/modules/collection/index.ts @@ -0,0 +1,9 @@ +import Elysia from "elysia"; +import { addItemToCollectionController } from "./controllers/addItemToCollection.controller"; +import { addItemToCollectionSchema } from "./schemas/addItemToCollection.schema"; + +export const collectionModule = new Elysia({ prefix: "/collections", tags: ["Collections"] }).post( + "/:name", + addItemToCollectionController, + addItemToCollectionSchema, +); diff --git a/src/modules/collection/schemas/addItemToCollection.schema.ts b/src/modules/collection/schemas/addItemToCollection.schema.ts new file mode 100644 index 0000000..36bda47 --- /dev/null +++ b/src/modules/collection/schemas/addItemToCollection.schema.ts @@ -0,0 +1,35 @@ +import { t } from "elysia"; +import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema"; + +export const addItemToCollectionSchema = { + headers: t.Object({ + cookie: t.String({ description: "Authentication token in cookie format, e.g., auth_token=your_jwt_token;" }), + }), + params: t.Object({ + name: t.String({ description: "Name of the collection to which the item will be added" }), + }), + body: t.Object({ + itemId: t.String({ description: "ID of the item to be added to the collection", examples: ["12345"] }), + }), + detail: { + summary: "Add an item to a collection", + description: "Adds a specified item to a collection identified by its name.", + responses: { + 200: { + description: "The item was successfully added to the collection.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { type: "boolean", example: true }, + status: { type: "number", example: 200 }, + message: { type: "string", example: "Item added to collection successfully" }, + }, + }, + }, + }, + }, + }, + }, +} satisfies AppRouteSchema; -- 2.49.0 From 3768ae4c264028274c284fad3fbd49a5f4d5ded1 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Sat, 28 Mar 2026 21:59:44 +0700 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20implement=20collectio?= =?UTF-8?q?n=20upsert=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/dbml/schema.dbml | 5 +++ .../addItemToCollection.controller.ts | 15 ++++++- .../UpsertUserCollection.repository.ts | 43 +++++++++++++++++++ .../services/addItemToCollection.service.ts | 15 +++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/modules/collection/repositories/UpsertUserCollection.repository.ts create mode 100644 src/modules/collection/services/addItemToCollection.service.ts diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index d546b66..2567404 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -369,6 +369,7 @@ Table user_logs { Table collections { id String [pk] name String [not null] + slug String [not null] medias medias [not null] owner users [not null] ownerId String [not null] @@ -379,6 +380,10 @@ Table collections { deletedAt DateTime createdAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null] + + indexes { + (slug, ownerId) [unique] + } } Table watch_histories { diff --git a/src/modules/collection/controllers/addItemToCollection.controller.ts b/src/modules/collection/controllers/addItemToCollection.controller.ts index 5024e0e..00a5885 100644 --- a/src/modules/collection/controllers/addItemToCollection.controller.ts +++ b/src/modules/collection/controllers/addItemToCollection.controller.ts @@ -1,10 +1,23 @@ import { Context, Static } from "elysia"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { addItemToCollectionSchema } from "../schemas/addItemToCollection.schema"; +import { addItemToCollectionService } from "../services/addItemToCollection.service"; +import { mainErrorHandler } from "../../../helpers/error/handler"; export const addItemToCollectionController = async (ctx: { set: Context["set"]; headers: Static; + params: Static; + body: Static; }) => { - return returnWriteResponse(ctx.set, 200, "Item added to collection successfully" + ctx.headers.cookie); + try { + const savedItem = await addItemToCollectionService({ + cookie: ctx.headers.cookie, + collectionName: ctx.params.name, + mediaId: ctx.body.itemId, + }); + return returnWriteResponse(ctx.set, 200, "Item added to collection successfully", savedItem); + } catch (error) { + return mainErrorHandler(ctx.set, error); + } }; diff --git a/src/modules/collection/repositories/UpsertUserCollection.repository.ts b/src/modules/collection/repositories/UpsertUserCollection.repository.ts new file mode 100644 index 0000000..5c8157b --- /dev/null +++ b/src/modules/collection/repositories/UpsertUserCollection.repository.ts @@ -0,0 +1,43 @@ +import slugify from "slugify"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; +import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; + +export interface UpsertUserCollectionRepositoryPayload { + userId: string; + collectionName: string; + mediaConnectId: string; +} + +export const upsertUserCollectionRepository = async (payload: UpsertUserCollectionRepositoryPayload) => { + try { + return await prisma.collection.upsert({ + where: { + slug_ownerId: { + slug: slugify(payload.collectionName, { lower: true }), + ownerId: payload.userId, + }, + }, + update: { + medias: { + connect: { + id: payload.mediaConnectId, + }, + }, + }, + create: { + id: generateUUIDv7(), + name: payload.collectionName, + slug: slugify(payload.collectionName, { lower: true }), + ownerId: payload.userId, + medias: { + connect: { + id: payload.mediaConnectId, + }, + }, + }, + }); + } catch (error) { + throw new AppError(500, "Failed to upsert user collection"); + } +}; diff --git a/src/modules/collection/services/addItemToCollection.service.ts b/src/modules/collection/services/addItemToCollection.service.ts new file mode 100644 index 0000000..dff1e06 --- /dev/null +++ b/src/modules/collection/services/addItemToCollection.service.ts @@ -0,0 +1,15 @@ +import { parse } from "cookie"; +import { jwtDecode } from "../../../helpers/http/jwt/decode"; +import { tokenValidationService } from "../../auth/services/http/tokenValidation.service"; +import slugify from "slugify"; + +export type AddItemToCollectionPayload = { + cookie: string; + collectionName: string; + mediaId: string; +}; + +export const addItemToCollectionService = async (payload: AddItemToCollectionPayload) => { + const { auth_token } = parse(payload.cookie); + return (await tokenValidationService(auth_token as string)).user.id; +}; -- 2.49.0 From f1e79945b0c690cce15e5b5ebda40f5cdc5a8226 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Sun, 29 Mar 2026 10:43:20 +0700 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20db:=20redesign=20co?= =?UTF-8?q?llection=20schema=20with=20manual=20pivot=20table=20to=20media?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/dbml/schema.dbml | 27 ++++++++++++--- .../migration.sql | 33 +++++++++++++++++++ prisma/schema.prisma | 22 +++++++++---- 3 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/20260329033709_create_table_pivot_for_media_collections/migration.sql diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 2567404..9abb82f 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -31,8 +31,8 @@ Table medias { bannerPromotion hero_banner [not null] logs media_logs [not null] episodes episodes [not null] - collections collections [not null] reviews movie_reviews [not null] + inCollections CollectionMedia [not null] } Table media_logs { @@ -370,7 +370,6 @@ Table collections { id String [pk] name String [not null] slug String [not null] - medias medias [not null] owner users [not null] ownerId String [not null] accessStatus AccessStatus [not null, default: 'private'] @@ -380,12 +379,26 @@ Table collections { deletedAt DateTime createdAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null] + media_saved CollectionMedia [not null] indexes { (slug, ownerId) [unique] } } +Table CollectionMedia { + id String [pk] + collection collections [not null] + collectionId String [not null] + media medias [not null] + mediaId String [not null] + savedAt DateTime [default: `now()`, not null] + + indexes { + (collectionId, mediaId) [unique] + } +} + Table watch_histories { id String [pk] episode episodes [not null] @@ -562,9 +575,9 @@ Table MediaCharacters { mediasId String [ref: > medias.id] } -Table MediaCollections { - collectionsId String [ref: > collections.id] - mediasId String [ref: > medias.id] +Table CollectionMedia { + incollectionsId String [ref: > CollectionMedia.id] + media_savedId String [ref: > CollectionMedia.id] } Table UserFavoriteGenres { @@ -752,6 +765,10 @@ Ref: user_logs.sessionId > user_sessions.id Ref: collections.ownerId > users.id +Ref: CollectionMedia.collectionId > collections.id + +Ref: CollectionMedia.mediaId > medias.id + Ref: watch_histories.id > episodes.id Ref: watch_histories.userId > users.id diff --git a/prisma/migrations/20260329033709_create_table_pivot_for_media_collections/migration.sql b/prisma/migrations/20260329033709_create_table_pivot_for_media_collections/migration.sql new file mode 100644 index 0000000..7d79d3c --- /dev/null +++ b/prisma/migrations/20260329033709_create_table_pivot_for_media_collections/migration.sql @@ -0,0 +1,33 @@ +/* + Warnings: + + - You are about to drop the `_MediaCollections` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "_MediaCollections" DROP CONSTRAINT "_MediaCollections_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_MediaCollections" DROP CONSTRAINT "_MediaCollections_B_fkey"; + +-- DropTable +DROP TABLE "_MediaCollections"; + +-- CreateTable +CREATE TABLE "CollectionMedia" ( + "id" UUID NOT NULL, + "collectionId" UUID NOT NULL, + "mediaId" UUID NOT NULL, + "savedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CollectionMedia_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "CollectionMedia_collectionId_mediaId_key" ON "CollectionMedia"("collectionId", "mediaId"); + +-- AddForeignKey +ALTER TABLE "CollectionMedia" ADD CONSTRAINT "CollectionMedia_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "collections"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CollectionMedia" ADD CONSTRAINT "CollectionMedia_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1c5f444..9ccae5b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,11 +49,11 @@ model Media { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - bannerPromotion HeroBanner[] @relation("MediaBannerPromotion") - logs MediaLog[] @relation("MediaLogs") - episodes Episode[] @relation("MediaEpisodes") - collections Collection[] @relation("MediaCollections") - reviews MediaReview[] @relation("MediaReviews") + bannerPromotion HeroBanner[] @relation("MediaBannerPromotion") + logs MediaLog[] @relation("MediaLogs") + episodes Episode[] @relation("MediaEpisodes") + reviews MediaReview[] @relation("MediaReviews") + inCollections CollectionMedia[] @relation("CollectionMedia") @@index([status, onDraft, deletedAt]) @@index([mediaType]) @@ -420,7 +420,6 @@ model Collection { id String @id @db.Uuid name String @db.VarChar(115) slug String @db.VarChar(115) - medias Media[] @relation("MediaCollections") owner User @relation("UserCollections", fields: [ownerId], references: [id]) ownerId String @db.Uuid accessStatus AccessStatus @default(private) @@ -431,10 +430,21 @@ model Collection { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + media_saved CollectionMedia[] @relation("CollectionMedia") @@unique([slug, ownerId]) @@map("collections") } +model CollectionMedia { + id String @id @db.Uuid + collection Collection @relation("CollectionMedia", fields: [collectionId], references: [id]) + collectionId String @db.Uuid + media Media @relation("CollectionMedia", fields: [mediaId], references: [id]) + mediaId String @db.Uuid + savedAt DateTime @default(now()) + @@unique([collectionId, mediaId]) +} + model WatchHistory { id String @id @db.Uuid episode Episode @relation("EpisodeWatchHistories", fields: [id], references: [id]) -- 2.49.0 From 73b22d7f2c5357070103d749693e2dd155914834 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Sun, 29 Mar 2026 11:17:52 +0700 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20collection=20modu?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...psertUserCollectionBySystem.repository.ts} | 31 ++++++++++++++----- .../services/addItemToCollection.service.ts | 19 +++++++++--- 2 files changed, 39 insertions(+), 11 deletions(-) rename src/modules/collection/repositories/{UpsertUserCollection.repository.ts => upsertUserCollectionBySystem.repository.ts} (54%) diff --git a/src/modules/collection/repositories/UpsertUserCollection.repository.ts b/src/modules/collection/repositories/upsertUserCollectionBySystem.repository.ts similarity index 54% rename from src/modules/collection/repositories/UpsertUserCollection.repository.ts rename to src/modules/collection/repositories/upsertUserCollectionBySystem.repository.ts index 5c8157b..22dec34 100644 --- a/src/modules/collection/repositories/UpsertUserCollection.repository.ts +++ b/src/modules/collection/repositories/upsertUserCollectionBySystem.repository.ts @@ -2,6 +2,7 @@ import slugify from "slugify"; import { AppError } from "../../../helpers/error/instances/app"; import { prisma } from "../../../utils/databases/prisma/connection"; import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; +import { Prisma } from "@prisma/client"; export interface UpsertUserCollectionRepositoryPayload { userId: string; @@ -9,7 +10,7 @@ export interface UpsertUserCollectionRepositoryPayload { mediaConnectId: string; } -export const upsertUserCollectionRepository = async (payload: UpsertUserCollectionRepositoryPayload) => { +export const upsertUserCollectionBySystemRepository = async (payload: UpsertUserCollectionRepositoryPayload) => { try { return await prisma.collection.upsert({ where: { @@ -19,9 +20,14 @@ export const upsertUserCollectionRepository = async (payload: UpsertUserCollecti }, }, update: { - medias: { - connect: { - id: payload.mediaConnectId, + media_saved: { + create: { + id: generateUUIDv7(), + media: { + connect: { + id: payload.mediaConnectId, + }, + }, }, }, }, @@ -29,15 +35,26 @@ export const upsertUserCollectionRepository = async (payload: UpsertUserCollecti id: generateUUIDv7(), name: payload.collectionName, slug: slugify(payload.collectionName, { lower: true }), - ownerId: payload.userId, - medias: { + owner: { connect: { - id: payload.mediaConnectId, + id: payload.userId, + }, + }, + media_saved: { + create: { + id: generateUUIDv7(), + media: { + connect: { + id: payload.mediaConnectId, + }, + }, }, }, }, }); } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") + throw new AppError(400, "Media item is already in the collection"); throw new AppError(500, "Failed to upsert user collection"); } }; diff --git a/src/modules/collection/services/addItemToCollection.service.ts b/src/modules/collection/services/addItemToCollection.service.ts index dff1e06..18318aa 100644 --- a/src/modules/collection/services/addItemToCollection.service.ts +++ b/src/modules/collection/services/addItemToCollection.service.ts @@ -1,7 +1,7 @@ import { parse } from "cookie"; -import { jwtDecode } from "../../../helpers/http/jwt/decode"; import { tokenValidationService } from "../../auth/services/http/tokenValidation.service"; -import slugify from "slugify"; +import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; +import { upsertUserCollectionBySystemRepository } from "../repositories/upsertUserCollectionBySystem.repository"; export type AddItemToCollectionPayload = { cookie: string; @@ -10,6 +10,17 @@ export type AddItemToCollectionPayload = { }; export const addItemToCollectionService = async (payload: AddItemToCollectionPayload) => { - const { auth_token } = parse(payload.cookie); - return (await tokenValidationService(auth_token as string)).user.id; + try { + const { auth_token } = parse(payload.cookie); + const userData = await tokenValidationService(auth_token as string); + const saveMediaToCollection = await upsertUserCollectionBySystemRepository({ + userId: userData.user.id, + collectionName: payload.collectionName, + mediaConnectId: payload.mediaId, + }); + + return saveMediaToCollection; + } catch (error) { + ErrorForwarder(error); + } }; -- 2.49.0 From 86fe39f5b5032ed8d32ba50e94c2be69498142bc Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Sun, 29 Mar 2026 11:23:27 +0700 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=9A=9A=20chore:=20move=20collection?= =?UTF-8?q?=20route=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../collection/controllers/addItemToCollection.controller.ts | 3 +-- src/modules/collection/index.ts | 2 +- src/modules/collection/schemas/addItemToCollection.schema.ts | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/collection/controllers/addItemToCollection.controller.ts b/src/modules/collection/controllers/addItemToCollection.controller.ts index 00a5885..b82a9a6 100644 --- a/src/modules/collection/controllers/addItemToCollection.controller.ts +++ b/src/modules/collection/controllers/addItemToCollection.controller.ts @@ -7,13 +7,12 @@ import { mainErrorHandler } from "../../../helpers/error/handler"; export const addItemToCollectionController = async (ctx: { set: Context["set"]; headers: Static; - params: Static; body: Static; }) => { try { const savedItem = await addItemToCollectionService({ cookie: ctx.headers.cookie, - collectionName: ctx.params.name, + collectionName: ctx.body.name, mediaId: ctx.body.itemId, }); return returnWriteResponse(ctx.set, 200, "Item added to collection successfully", savedItem); diff --git a/src/modules/collection/index.ts b/src/modules/collection/index.ts index 781d87c..943f510 100644 --- a/src/modules/collection/index.ts +++ b/src/modules/collection/index.ts @@ -3,7 +3,7 @@ import { addItemToCollectionController } from "./controllers/addItemToCollection import { addItemToCollectionSchema } from "./schemas/addItemToCollection.schema"; export const collectionModule = new Elysia({ prefix: "/collections", tags: ["Collections"] }).post( - "/:name", + "/sys", addItemToCollectionController, addItemToCollectionSchema, ); diff --git a/src/modules/collection/schemas/addItemToCollection.schema.ts b/src/modules/collection/schemas/addItemToCollection.schema.ts index 36bda47..cd5d740 100644 --- a/src/modules/collection/schemas/addItemToCollection.schema.ts +++ b/src/modules/collection/schemas/addItemToCollection.schema.ts @@ -5,10 +5,8 @@ export const addItemToCollectionSchema = { headers: t.Object({ cookie: t.String({ description: "Authentication token in cookie format, e.g., auth_token=your_jwt_token;" }), }), - params: t.Object({ - name: t.String({ description: "Name of the collection to which the item will be added" }), - }), body: t.Object({ + name: t.String({ description: "Name of the collection to which the item will be added" }), itemId: t.String({ description: "ID of the item to be added to the collection", examples: ["12345"] }), }), detail: { -- 2.49.0 From 68d834ae6b43c8014fec4bca87c9e0315d5d0af2 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Sun, 29 Mar 2026 12:10:50 +0700 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20delete=20endpoint?= =?UTF-8?q?=20to=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ... addItemToCollectionBySytem.controller.ts} | 12 +++---- ...oveItemFromCollectionBySytem.controller.ts | 22 +++++++++++++ src/modules/collection/index.ts | 14 ++++---- ...ItemInUserCollectionBySystem.repository.ts | 32 ++++++++++++++++++ ...s => addItemToCollectionBySytem.schema.ts} | 2 +- .../removeItemFromCollectionBySytem.schema.ts | 33 +++++++++++++++++++ ...=> addItemToCollectionBySystem.service.ts} | 6 ++-- ...emoveItemFromCollectionBySystem.service.ts | 24 ++++++++++++++ 8 files changed, 127 insertions(+), 18 deletions(-) rename src/modules/collection/controllers/{addItemToCollection.controller.ts => addItemToCollectionBySytem.controller.ts} (52%) create mode 100644 src/modules/collection/controllers/removeItemFromCollectionBySytem.controller.ts create mode 100644 src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts rename src/modules/collection/schemas/{addItemToCollection.schema.ts => addItemToCollectionBySytem.schema.ts} (95%) create mode 100644 src/modules/collection/schemas/removeItemFromCollectionBySytem.schema.ts rename src/modules/collection/services/{addItemToCollection.service.ts => addItemToCollectionBySystem.service.ts} (78%) create mode 100644 src/modules/collection/services/removeItemFromCollectionBySystem.service.ts diff --git a/src/modules/collection/controllers/addItemToCollection.controller.ts b/src/modules/collection/controllers/addItemToCollectionBySytem.controller.ts similarity index 52% rename from src/modules/collection/controllers/addItemToCollection.controller.ts rename to src/modules/collection/controllers/addItemToCollectionBySytem.controller.ts index b82a9a6..50c3bb2 100644 --- a/src/modules/collection/controllers/addItemToCollection.controller.ts +++ b/src/modules/collection/controllers/addItemToCollectionBySytem.controller.ts @@ -1,16 +1,16 @@ import { Context, Static } from "elysia"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; -import { addItemToCollectionSchema } from "../schemas/addItemToCollection.schema"; -import { addItemToCollectionService } from "../services/addItemToCollection.service"; +import { addItemToCollectionBySytemSchema } from "../schemas/addItemToCollectionBySytem.schema"; +import { addItemToCollectionBySystemService } from "../services/addItemToCollectionBySystem.service"; import { mainErrorHandler } from "../../../helpers/error/handler"; -export const addItemToCollectionController = async (ctx: { +export const addItemToCollectionBySytemController = async (ctx: { set: Context["set"]; - headers: Static; - body: Static; + headers: Static; + body: Static; }) => { try { - const savedItem = await addItemToCollectionService({ + const savedItem = await addItemToCollectionBySystemService({ cookie: ctx.headers.cookie, collectionName: ctx.body.name, mediaId: ctx.body.itemId, diff --git a/src/modules/collection/controllers/removeItemFromCollectionBySytem.controller.ts b/src/modules/collection/controllers/removeItemFromCollectionBySytem.controller.ts new file mode 100644 index 0000000..fccdbd8 --- /dev/null +++ b/src/modules/collection/controllers/removeItemFromCollectionBySytem.controller.ts @@ -0,0 +1,22 @@ +import { Context, Static } from "elysia"; +import { mainErrorHandler } from "../../../helpers/error/handler"; +import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; +import { removeItemFromCollectionBySystemService } from "../services/removeItemFromCollectionBySystem.service"; +import { removeItemFromCollectionBySytemSchema } from "../schemas/removeItemFromCollectionBySytem.schema"; + +export const removeItemFromCollectionBySytemController = async (ctx: { + set: Context["set"]; + headers: Static; + body: Static; +}) => { + try { + const removedItem = await removeItemFromCollectionBySystemService({ + cookie: ctx.headers.cookie, + collectionName: ctx.body.name, + mediaId: ctx.body.itemId, + }); + return returnWriteResponse(ctx.set, 200, "Item removed from collection successfully", removedItem); + } catch (error) { + return mainErrorHandler(ctx.set, error); + } +}; diff --git a/src/modules/collection/index.ts b/src/modules/collection/index.ts index 943f510..4c768c5 100644 --- a/src/modules/collection/index.ts +++ b/src/modules/collection/index.ts @@ -1,9 +1,9 @@ import Elysia from "elysia"; -import { addItemToCollectionController } from "./controllers/addItemToCollection.controller"; -import { addItemToCollectionSchema } from "./schemas/addItemToCollection.schema"; +import { addItemToCollectionBySytemController } from "./controllers/addItemToCollectionBySytem.controller"; +import { addItemToCollectionBySytemSchema } from "./schemas/addItemToCollectionBySytem.schema"; +import { removeItemFromCollectionBySytemController } from "./controllers/removeItemFromCollectionBySytem.controller"; +import { removeItemFromCollectionBySytemSchema } from "./schemas/removeItemFromCollectionBySytem.schema"; -export const collectionModule = new Elysia({ prefix: "/collections", tags: ["Collections"] }).post( - "/sys", - addItemToCollectionController, - addItemToCollectionSchema, -); +export const collectionModule = new Elysia({ prefix: "/collections", tags: ["Collections"] }) + .post("/sys", addItemToCollectionBySytemController, addItemToCollectionBySytemSchema) + .delete("/sys", removeItemFromCollectionBySytemController, removeItemFromCollectionBySytemSchema); diff --git a/src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts b/src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts new file mode 100644 index 0000000..16c3cab --- /dev/null +++ b/src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts @@ -0,0 +1,32 @@ +import slugify from "slugify"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; +import { Prisma } from "@prisma/client"; + +export type DeleteUserCollectionBySystemPayload = { + userId: string; + collectionName: string; + itemId: string; +}; + +export const deleteItemInUserCollectionBySystemRepository = async (payload: DeleteUserCollectionBySystemPayload) => { + try { + return await prisma.collection.update({ + where: { + slug_ownerId: { + slug: slugify(payload.collectionName, { lower: true }), + ownerId: payload.userId, + }, + }, + data: { + media_saved: { + deleteMany: { + mediaId: payload.itemId, + }, + }, + }, + }); + } catch (error) { + throw new AppError(500, "Failed to remove item from collection"); + } +}; diff --git a/src/modules/collection/schemas/addItemToCollection.schema.ts b/src/modules/collection/schemas/addItemToCollectionBySytem.schema.ts similarity index 95% rename from src/modules/collection/schemas/addItemToCollection.schema.ts rename to src/modules/collection/schemas/addItemToCollectionBySytem.schema.ts index cd5d740..35b4574 100644 --- a/src/modules/collection/schemas/addItemToCollection.schema.ts +++ b/src/modules/collection/schemas/addItemToCollectionBySytem.schema.ts @@ -1,7 +1,7 @@ import { t } from "elysia"; import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema"; -export const addItemToCollectionSchema = { +export const addItemToCollectionBySytemSchema = { headers: t.Object({ cookie: t.String({ description: "Authentication token in cookie format, e.g., auth_token=your_jwt_token;" }), }), diff --git a/src/modules/collection/schemas/removeItemFromCollectionBySytem.schema.ts b/src/modules/collection/schemas/removeItemFromCollectionBySytem.schema.ts new file mode 100644 index 0000000..54a4382 --- /dev/null +++ b/src/modules/collection/schemas/removeItemFromCollectionBySytem.schema.ts @@ -0,0 +1,33 @@ +import { t } from "elysia"; +import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema"; + +export const removeItemFromCollectionBySytemSchema = { + headers: t.Object({ + cookie: t.String({ description: "Authentication token in cookie format, e.g., auth_token=your_jwt_token;" }), + }), + body: t.Object({ + name: t.String({ description: "Name of the collection to which the item will be added" }), + itemId: t.String({ description: "ID of the item to be added to the collection", examples: ["12345"] }), + }), + detail: { + summary: "Remove an item from a collection", + description: "Removes a specified item from a collection identified by its name.", + responses: { + 200: { + description: "The item was successfully removed from the collection.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { type: "boolean", example: true }, + status: { type: "number", example: 200 }, + message: { type: "string", example: "Item removed from collection successfully" }, + }, + }, + }, + }, + }, + }, + }, +} satisfies AppRouteSchema; diff --git a/src/modules/collection/services/addItemToCollection.service.ts b/src/modules/collection/services/addItemToCollectionBySystem.service.ts similarity index 78% rename from src/modules/collection/services/addItemToCollection.service.ts rename to src/modules/collection/services/addItemToCollectionBySystem.service.ts index 18318aa..ca5f3ff 100644 --- a/src/modules/collection/services/addItemToCollection.service.ts +++ b/src/modules/collection/services/addItemToCollectionBySystem.service.ts @@ -9,17 +9,15 @@ export type AddItemToCollectionPayload = { mediaId: string; }; -export const addItemToCollectionService = async (payload: AddItemToCollectionPayload) => { +export const addItemToCollectionBySystemService = async (payload: AddItemToCollectionPayload) => { try { const { auth_token } = parse(payload.cookie); const userData = await tokenValidationService(auth_token as string); - const saveMediaToCollection = await upsertUserCollectionBySystemRepository({ + return await upsertUserCollectionBySystemRepository({ userId: userData.user.id, collectionName: payload.collectionName, mediaConnectId: payload.mediaId, }); - - return saveMediaToCollection; } catch (error) { ErrorForwarder(error); } diff --git a/src/modules/collection/services/removeItemFromCollectionBySystem.service.ts b/src/modules/collection/services/removeItemFromCollectionBySystem.service.ts new file mode 100644 index 0000000..a3e200d --- /dev/null +++ b/src/modules/collection/services/removeItemFromCollectionBySystem.service.ts @@ -0,0 +1,24 @@ +import { parse } from "cookie"; +import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; +import { tokenValidationService } from "../../auth/services/http/tokenValidation.service"; +import { deleteItemInUserCollectionBySystemRepository } from "../repositories/deleteItemInUserCollectionBySystem.repository"; + +export type RemoveItemFromCollectionPayload = { + cookie: string; + collectionName: string; + mediaId: string; +}; + +export const removeItemFromCollectionBySystemService = async (payload: RemoveItemFromCollectionPayload) => { + try { + const { auth_token } = parse(payload.cookie); + const { user } = await tokenValidationService(auth_token as string); + return await deleteItemInUserCollectionBySystemRepository({ + userId: user.id, + collectionName: payload.collectionName, + itemId: payload.mediaId, + }); + } catch (error) { + ErrorForwarder(error); + } +}; -- 2.49.0 From 412b501c803e5ff3cb007a2d9eee0696977024fa Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Sun, 29 Mar 2026 14:17:42 +0700 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=9A=A8=20fix:=20resolve=20linting=20t?= =?UTF-8?q?ype=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deleteItemInUserCollectionBySystem.repository.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts b/src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts index 16c3cab..7f20a79 100644 --- a/src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts +++ b/src/modules/collection/repositories/deleteItemInUserCollectionBySystem.repository.ts @@ -1,7 +1,6 @@ import slugify from "slugify"; import { AppError } from "../../../helpers/error/instances/app"; import { prisma } from "../../../utils/databases/prisma/connection"; -import { Prisma } from "@prisma/client"; export type DeleteUserCollectionBySystemPayload = { userId: string; @@ -27,6 +26,6 @@ export const deleteItemInUserCollectionBySystemRepository = async (payload: Dele }, }); } catch (error) { - throw new AppError(500, "Failed to remove item from collection"); + throw new AppError(500, "Failed to remove item from collection", error); } }; -- 2.49.0