feat/collection #29
@ -31,8 +31,8 @@ Table medias {
|
|||||||
bannerPromotion hero_banner [not null]
|
bannerPromotion hero_banner [not null]
|
||||||
logs media_logs [not null]
|
logs media_logs [not null]
|
||||||
episodes episodes [not null]
|
episodes episodes [not null]
|
||||||
collections collections [not null]
|
|
||||||
reviews movie_reviews [not null]
|
reviews movie_reviews [not null]
|
||||||
|
inCollections CollectionMedia [not null]
|
||||||
}
|
}
|
||||||
|
|
||||||
Table media_logs {
|
Table media_logs {
|
||||||
@ -369,7 +369,7 @@ Table user_logs {
|
|||||||
Table collections {
|
Table collections {
|
||||||
id String [pk]
|
id String [pk]
|
||||||
name String [not null]
|
name String [not null]
|
||||||
medias medias [not null]
|
slug String [not null]
|
||||||
owner users [not null]
|
owner users [not null]
|
||||||
ownerId String [not null]
|
ownerId String [not null]
|
||||||
accessStatus AccessStatus [not null, default: 'private']
|
accessStatus AccessStatus [not null, default: 'private']
|
||||||
@ -379,6 +379,24 @@ Table collections {
|
|||||||
deletedAt DateTime
|
deletedAt DateTime
|
||||||
createdAt DateTime [default: `now()`, not null]
|
createdAt DateTime [default: `now()`, not null]
|
||||||
updatedAt 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 {
|
Table watch_histories {
|
||||||
@ -557,9 +575,9 @@ Table MediaCharacters {
|
|||||||
mediasId String [ref: > medias.id]
|
mediasId String [ref: > medias.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
Table MediaCollections {
|
Table CollectionMedia {
|
||||||
collectionsId String [ref: > collections.id]
|
incollectionsId String [ref: > CollectionMedia.id]
|
||||||
mediasId String [ref: > medias.id]
|
media_savedId String [ref: > CollectionMedia.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
Table UserFavoriteGenres {
|
Table UserFavoriteGenres {
|
||||||
@ -747,6 +765,10 @@ Ref: user_logs.sessionId > user_sessions.id
|
|||||||
|
|
||||||
Ref: collections.ownerId > users.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.id > episodes.id
|
||||||
|
|
||||||
Ref: watch_histories.userId > users.id
|
Ref: watch_histories.userId > users.id
|
||||||
|
|||||||
@ -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");
|
||||||
@ -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;
|
||||||
@ -52,8 +52,8 @@ model Media {
|
|||||||
bannerPromotion HeroBanner[] @relation("MediaBannerPromotion")
|
bannerPromotion HeroBanner[] @relation("MediaBannerPromotion")
|
||||||
logs MediaLog[] @relation("MediaLogs")
|
logs MediaLog[] @relation("MediaLogs")
|
||||||
episodes Episode[] @relation("MediaEpisodes")
|
episodes Episode[] @relation("MediaEpisodes")
|
||||||
collections Collection[] @relation("MediaCollections")
|
|
||||||
reviews MediaReview[] @relation("MediaReviews")
|
reviews MediaReview[] @relation("MediaReviews")
|
||||||
|
inCollections CollectionMedia[] @relation("CollectionMedia")
|
||||||
|
|
||||||
@@index([status, onDraft, deletedAt])
|
@@index([status, onDraft, deletedAt])
|
||||||
@@index([mediaType])
|
@@index([mediaType])
|
||||||
@ -418,8 +418,8 @@ model UserLog {
|
|||||||
|
|
||||||
model Collection {
|
model Collection {
|
||||||
id String @id @db.Uuid
|
id String @id @db.Uuid
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(115)
|
||||||
medias Media[] @relation("MediaCollections")
|
slug String @db.VarChar(115)
|
||||||
owner User @relation("UserCollections", fields: [ownerId], references: [id])
|
owner User @relation("UserCollections", fields: [ownerId], references: [id])
|
||||||
ownerId String @db.Uuid
|
ownerId String @db.Uuid
|
||||||
accessStatus AccessStatus @default(private)
|
accessStatus AccessStatus @default(private)
|
||||||
@ -429,9 +429,22 @@ model Collection {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
media_saved CollectionMedia[] @relation("CollectionMedia")
|
||||||
|
@@unique([slug, ownerId])
|
||||||
@@map("collections")
|
@@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 {
|
model WatchHistory {
|
||||||
id String @id @db.Uuid
|
id String @id @db.Uuid
|
||||||
episode Episode @relation("EpisodeWatchHistories", fields: [id], references: [id])
|
episode Episode @relation("EpisodeWatchHistories", fields: [id], references: [id])
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { Context, Static } from "elysia";
|
||||||
|
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
import { addItemToCollectionBySytemSchema } from "../schemas/addItemToCollectionBySytem.schema";
|
||||||
|
import { addItemToCollectionBySystemService } from "../services/addItemToCollectionBySystem.service";
|
||||||
|
import { mainErrorHandler } from "../../../helpers/error/handler";
|
||||||
|
|
||||||
|
export const addItemToCollectionBySytemController = async (ctx: {
|
||||||
|
set: Context["set"];
|
||||||
|
headers: Static<typeof addItemToCollectionBySytemSchema.headers>;
|
||||||
|
body: Static<typeof addItemToCollectionBySytemSchema.body>;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const savedItem = await addItemToCollectionBySystemService({
|
||||||
|
cookie: ctx.headers.cookie,
|
||||||
|
collectionName: ctx.body.name,
|
||||||
|
mediaId: ctx.body.itemId,
|
||||||
|
});
|
||||||
|
return returnWriteResponse(ctx.set, 200, "Item added to collection successfully", savedItem);
|
||||||
|
} catch (error) {
|
||||||
|
return mainErrorHandler(ctx.set, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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<typeof removeItemFromCollectionBySytemSchema.headers>;
|
||||||
|
body: Static<typeof removeItemFromCollectionBySytemSchema.body>;
|
||||||
|
}) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
9
src/modules/collection/index.ts
Normal file
9
src/modules/collection/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Elysia from "elysia";
|
||||||
|
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", addItemToCollectionBySytemController, addItemToCollectionBySytemSchema)
|
||||||
|
.delete("/sys", removeItemFromCollectionBySytemController, removeItemFromCollectionBySytemSchema);
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import slugify from "slugify";
|
||||||
|
import { AppError } from "../../../helpers/error/instances/app";
|
||||||
|
import { prisma } from "../../../utils/databases/prisma/connection";
|
||||||
|
|
||||||
|
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", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
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;
|
||||||
|
collectionName: string;
|
||||||
|
mediaConnectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const upsertUserCollectionBySystemRepository = async (payload: UpsertUserCollectionRepositoryPayload) => {
|
||||||
|
try {
|
||||||
|
return await prisma.collection.upsert({
|
||||||
|
where: {
|
||||||
|
slug_ownerId: {
|
||||||
|
slug: slugify(payload.collectionName, { lower: true }),
|
||||||
|
ownerId: payload.userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
media_saved: {
|
||||||
|
create: {
|
||||||
|
id: generateUUIDv7(),
|
||||||
|
media: {
|
||||||
|
connect: {
|
||||||
|
id: payload.mediaConnectId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: generateUUIDv7(),
|
||||||
|
name: payload.collectionName,
|
||||||
|
slug: slugify(payload.collectionName, { lower: true }),
|
||||||
|
owner: {
|
||||||
|
connect: {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
|
||||||
|
|
||||||
|
export const addItemToCollectionBySytemSchema = {
|
||||||
|
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: "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;
|
||||||
@ -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;
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { parse } from "cookie";
|
||||||
|
import { tokenValidationService } from "../../auth/services/http/tokenValidation.service";
|
||||||
|
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
|
||||||
|
import { upsertUserCollectionBySystemRepository } from "../repositories/upsertUserCollectionBySystem.repository";
|
||||||
|
|
||||||
|
export type AddItemToCollectionPayload = {
|
||||||
|
cookie: string;
|
||||||
|
collectionName: string;
|
||||||
|
mediaId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addItemToCollectionBySystemService = async (payload: AddItemToCollectionPayload) => {
|
||||||
|
try {
|
||||||
|
const { auth_token } = parse(payload.cookie);
|
||||||
|
const userData = await tokenValidationService(auth_token as string);
|
||||||
|
return await upsertUserCollectionBySystemRepository({
|
||||||
|
userId: userData.user.id,
|
||||||
|
collectionName: payload.collectionName,
|
||||||
|
mediaConnectId: payload.mediaId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
ErrorForwarder(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user