Compare commits

..

18 Commits

Author SHA1 Message Date
6556ac8354 Merge pull request 'fix-media' (#32) from fix-media into main
All checks were successful
Sync to GitHub / sync (push) Successful in 10s
Reviewed-on: #32
2026-04-24 10:31:42 +07:00
16d8c00d75 🗃️ db: fix schema drift and correct season enum
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 1m14s
2026-04-22 12:18:56 +07:00
e61686956b Merge pull request 'media-detail' (#31) from media-detail into main
All checks were successful
Sync to GitHub / sync (push) Successful in 9s
Reviewed-on: #31
2026-04-21 20:44:51 +07:00
baf7ba20b1 🗃️ db: enforce season field constraint 2026-04-07 12:00:00 +07:00
697f28de60 🗃️ db: add season field to media model 2026-04-03 12:00:00 +07:00
72f8e9e4eb 👔 feat: implement database repository for get media by slug
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 1m51s
2026-04-02 09:48:31 +07:00
59228f7d1e feat: add service for getBySlug 2026-04-02 09:34:03 +07:00
b27479cd3e 🐛 fix: resolve schema type error in getAllMedia module 2026-04-01 23:38:32 +07:00
17eb272b1d feat: add endpoint to fetch media by slug 2026-04-01 23:28:22 +07:00
697374d6cd Merge pull request '👔 feat: support user collection check on hero banner' (#30) from fix-banner into main
All checks were successful
Sync to GitHub / sync (push) Successful in 9s
Reviewed-on: #30
2026-03-30 19:21:12 +07:00
5cebd200c4 👔 feat: support user collection check on hero banner
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 56s
2026-03-30 19:03:37 +07:00
cef6da16cb Merge pull request 'feat/collection' (#29) from feat/collection into main
All checks were successful
Sync to GitHub / sync (push) Successful in 7s
Reviewed-on: #29
2026-03-29 14:18:30 +07:00
412b501c80 🚨 fix: resolve linting type error
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 31s
2026-03-29 14:17:42 +07:00
68d834ae6b feat: add delete endpoint to collection
Some checks failed
Integration Tests / integration-tests (pull_request) Failing after 1m24s
2026-03-29 12:10:50 +07:00
86fe39f5b5 🚚 chore: move collection route module 2026-03-29 11:23:27 +07:00
73b22d7f2c feat: add collection module 2026-03-29 11:17:52 +07:00
f1e79945b0 🗃️ db: redesign collection schema with manual pivot table to media 2026-03-29 10:43:20 +07:00
3768ae4c26 🚧 wip: implement collection upsert logic 2026-03-28 21:59:44 +07:00
33 changed files with 1262 additions and 905 deletions

View File

@ -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

View File

@ -7,6 +7,9 @@ CREATE TYPE "MediaType" AS ENUM ('TV', 'ONA', 'OVA', 'Movie', 'Special', 'Music'
-- CreateEnum -- CreateEnum
CREATE TYPE "Country" AS ENUM ('Japanese', 'English', 'Indonesia', 'Korea'); CREATE TYPE "Country" AS ENUM ('Japanese', 'English', 'Indonesia', 'Korea');
-- CreateEnum
CREATE TYPE "Season" AS ENUM ('Winter', 'Spring', 'Summer', 'Fall');
-- CreateEnum -- CreateEnum
CREATE TYPE "CharacterRole" AS ENUM ('Main', 'Supporting'); CREATE TYPE "CharacterRole" AS ENUM ('Main', 'Supporting');
@ -72,6 +75,8 @@ CREATE TABLE "medias" (
"deletedAt" TIMESTAMP(3), "deletedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"season" "Season",
"yearReleased" SMALLINT NOT NULL,
CONSTRAINT "medias_pkey" PRIMARY KEY ("id") CONSTRAINT "medias_pkey" PRIMARY KEY ("id")
); );
@ -367,7 +372,7 @@ CREATE TABLE "user_logs" (
-- CreateTable -- CreateTable
CREATE TABLE "collections" ( CREATE TABLE "collections" (
"id" UUID NOT NULL, "id" UUID NOT NULL,
"name" VARCHAR(255) NOT NULL, "name" VARCHAR(115) NOT NULL,
"ownerId" UUID NOT NULL, "ownerId" UUID NOT NULL,
"accessStatus" "AccessStatus" NOT NULL DEFAULT 'private', "accessStatus" "AccessStatus" NOT NULL DEFAULT 'private',
"password" VARCHAR(255), "password" VARCHAR(255),
@ -375,10 +380,21 @@ CREATE TABLE "collections" (
"deletedAt" TIMESTAMP(3), "deletedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"slug" VARCHAR(115) NOT NULL,
CONSTRAINT "collections_pkey" PRIMARY KEY ("id") CONSTRAINT "collections_pkey" PRIMARY KEY ("id")
); );
-- 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")
);
-- CreateTable -- CreateTable
CREATE TABLE "watch_histories" ( CREATE TABLE "watch_histories" (
"id" UUID NOT NULL, "id" UUID NOT NULL,
@ -502,18 +518,13 @@ CREATE TABLE "email_system_histories" (
CREATE TABLE "hero_banner" ( CREATE TABLE "hero_banner" (
"id" UUID NOT NULL, "id" UUID NOT NULL,
"orderPriority" INTEGER, "orderPriority" INTEGER,
"isClickable" BOOLEAN NOT NULL DEFAULT false,
"title" VARCHAR(225),
"tags" TEXT[],
"description" TEXT,
"buttonContent" VARCHAR(100),
"buttonLink" TEXT,
"imageUrl" TEXT, "imageUrl" TEXT,
"startDate" TIMESTAMP(3) NOT NULL, "startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL, "endDate" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creatorId" UUID NOT NULL, "creatorId" UUID NOT NULL,
"mediaId" UUID NOT NULL,
CONSTRAINT "hero_banner_pkey" PRIMARY KEY ("id") CONSTRAINT "hero_banner_pkey" PRIMARY KEY ("id")
); );
@ -591,14 +602,6 @@ CREATE TABLE "_MediaCharacters" (
CONSTRAINT "_MediaCharacters_AB_pkey" PRIMARY KEY ("A","B") CONSTRAINT "_MediaCharacters_AB_pkey" PRIMARY KEY ("A","B")
); );
-- CreateTable
CREATE TABLE "_MediaCollections" (
"A" UUID NOT NULL,
"B" UUID NOT NULL,
CONSTRAINT "_MediaCollections_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable -- CreateTable
CREATE TABLE "_UserSelectedSharingCollention" ( CREATE TABLE "_UserSelectedSharingCollention" (
"A" UUID NOT NULL, "A" UUID NOT NULL,
@ -673,6 +676,12 @@ CREATE UNIQUE INDEX "user_roles_name_key" ON "user_roles"("name");
-- CreateIndex -- CreateIndex
CREATE INDEX "user_sessions_userId_isAuthenticated_deletedAt_idx" ON "user_sessions"("userId", "isAuthenticated", "deletedAt"); CREATE INDEX "user_sessions_userId_isAuthenticated_deletedAt_idx" ON "user_sessions"("userId", "isAuthenticated", "deletedAt");
-- CreateIndex
CREATE UNIQUE INDEX "collections_slug_ownerId_key" ON "collections"("slug", "ownerId");
-- CreateIndex
CREATE UNIQUE INDEX "CollectionMedia_collectionId_mediaId_key" ON "CollectionMedia"("collectionId", "mediaId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "languages_code_key" ON "languages"("code"); CREATE UNIQUE INDEX "languages_code_key" ON "languages"("code");
@ -703,24 +712,21 @@ CREATE INDEX "_UserFavoriteGenres_B_index" ON "_UserFavoriteGenres"("B");
-- CreateIndex -- CreateIndex
CREATE INDEX "_MediaCharacters_B_index" ON "_MediaCharacters"("B"); CREATE INDEX "_MediaCharacters_B_index" ON "_MediaCharacters"("B");
-- CreateIndex
CREATE INDEX "_MediaCollections_B_index" ON "_MediaCollections"("B");
-- CreateIndex -- CreateIndex
CREATE INDEX "_UserSelectedSharingCollention_B_index" ON "_UserSelectedSharingCollention"("B"); CREATE INDEX "_UserSelectedSharingCollention_B_index" ON "_UserSelectedSharingCollention"("B");
-- AddForeignKey -- AddForeignKey
ALTER TABLE "medias" ADD CONSTRAINT "medias_uploadedBy_fkey" FOREIGN KEY ("uploadedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "medias" ADD CONSTRAINT "medias_uploadedBy_fkey" FOREIGN KEY ("uploadedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_logs" ADD CONSTRAINT "media_logs_proposedBy_fkey" FOREIGN KEY ("proposedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "media_logs" ADD CONSTRAINT "media_logs_approvedBy_fkey" FOREIGN KEY ("approvedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "media_logs" ADD CONSTRAINT "media_logs_approvedBy_fkey" FOREIGN KEY ("approvedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "media_logs" ADD CONSTRAINT "media_logs_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "media_logs" ADD CONSTRAINT "media_logs_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_logs" ADD CONSTRAINT "media_logs_proposedBy_fkey" FOREIGN KEY ("proposedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "genres" ADD CONSTRAINT "genres_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "genres" ADD CONSTRAINT "genres_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@ -733,15 +739,15 @@ ALTER TABLE "characters" ADD CONSTRAINT "characters_creatorId_fkey" FOREIGN KEY
-- AddForeignKey -- AddForeignKey
ALTER TABLE "voice_actors" ADD CONSTRAINT "voice_actors_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "voice_actors" ADD CONSTRAINT "voice_actors_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lang_va_char" ADD CONSTRAINT "lang_va_char_vaId_fkey" FOREIGN KEY ("vaId") REFERENCES "voice_actors"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "lang_va_char" ADD CONSTRAINT "lang_va_char_charId_fkey" FOREIGN KEY ("charId") REFERENCES "characters"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "lang_va_char" ADD CONSTRAINT "lang_va_char_charId_fkey" FOREIGN KEY ("charId") REFERENCES "characters"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "lang_va_char" ADD CONSTRAINT "lang_va_char_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "lang_va_char" ADD CONSTRAINT "lang_va_char_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lang_va_char" ADD CONSTRAINT "lang_va_char_vaId_fkey" FOREIGN KEY ("vaId") REFERENCES "voice_actors"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "episodes" ADD CONSTRAINT "episodes_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "episodes" ADD CONSTRAINT "episodes_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@ -749,13 +755,13 @@ ALTER TABLE "episodes" ADD CONSTRAINT "episodes_mediaId_fkey" FOREIGN KEY ("medi
ALTER TABLE "episodes" ADD CONSTRAINT "episodes_uploadedBy_fkey" FOREIGN KEY ("uploadedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "episodes" ADD CONSTRAINT "episodes_uploadedBy_fkey" FOREIGN KEY ("uploadedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "episode_likes" ADD CONSTRAINT "episode_likes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "episode_likes" ADD CONSTRAINT "episode_likes_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "episode_likes" ADD CONSTRAINT "episode_likes_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "user_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "episode_likes" ADD CONSTRAINT "episode_likes_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "user_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "episode_likes" ADD CONSTRAINT "episode_likes_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "episode_likes" ADD CONSTRAINT "episode_likes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "videos" ADD CONSTRAINT "videos_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "videos" ADD CONSTRAINT "videos_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@ -769,23 +775,23 @@ ALTER TABLE "videos" ADD CONSTRAINT "videos_uploadedBy_fkey" FOREIGN KEY ("uploa
-- AddForeignKey -- AddForeignKey
ALTER TABLE "video_services" ADD CONSTRAINT "video_services_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "video_services" ADD CONSTRAINT "video_services_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_langPreference_fkey" FOREIGN KEY ("langPreference") REFERENCES "languages"("code") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_langPreference_fkey" FOREIGN KEY ("langPreference") REFERENCES "languages"("code") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_serviceDefaultId_fkey" FOREIGN KEY ("serviceDefaultId") REFERENCES "video_services"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_serviceDefaultId_fkey" FOREIGN KEY ("serviceDefaultId") REFERENCES "video_services"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_role_assignments" ADD CONSTRAINT "user_role_assignments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "user_role_assignments" ADD CONSTRAINT "user_role_assignments_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "user_roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_role_assignments" ADD CONSTRAINT "user_role_assignments_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "user_roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "user_role_assignments" ADD CONSTRAINT "user_role_assignments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_notifications" ADD CONSTRAINT "user_notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "user_notifications" ADD CONSTRAINT "user_notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@ -794,29 +800,35 @@ ALTER TABLE "user_notifications" ADD CONSTRAINT "user_notifications_userId_fkey"
ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_logs" ADD CONSTRAINT "user_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "user_logs" ADD CONSTRAINT "user_logs_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "user_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "user_logs" ADD CONSTRAINT "user_logs_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "user_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "user_logs" ADD CONSTRAINT "user_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "collections" ADD CONSTRAINT "collections_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "collections" ADD CONSTRAINT "collections_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "watch_histories" ADD CONSTRAINT "watch_histories_id_fkey" FOREIGN KEY ("id") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "CollectionMedia" ADD CONSTRAINT "CollectionMedia_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "collections"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "watch_histories" ADD CONSTRAINT "watch_histories_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "CollectionMedia" ADD CONSTRAINT "CollectionMedia_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "watch_histories" ADD CONSTRAINT "watch_histories_id_fkey" FOREIGN KEY ("id") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "watch_histories" ADD CONSTRAINT "watch_histories_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "user_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "watch_histories" ADD CONSTRAINT "watch_histories_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "user_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "movie_reviews" ADD CONSTRAINT "movie_reviews_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "watch_histories" ADD CONSTRAINT "watch_histories_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "movie_reviews" ADD CONSTRAINT "movie_reviews_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "movie_reviews" ADD CONSTRAINT "movie_reviews_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "movie_reviews" ADD CONSTRAINT "movie_reviews_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "comments" ADD CONSTRAINT "comments_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "comments" ADD CONSTRAINT "comments_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@ -833,10 +845,10 @@ ALTER TABLE "comment_likes" ADD CONSTRAINT "comment_likes_commentId_fkey" FOREIG
ALTER TABLE "comment_likes" ADD CONSTRAINT "comment_likes_userLiked_fkey" FOREIGN KEY ("userLiked") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "comment_likes" ADD CONSTRAINT "comment_likes_userLiked_fkey" FOREIGN KEY ("userLiked") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "comment_reports" ADD CONSTRAINT "comment_reports_userReporter_fkey" FOREIGN KEY ("userReporter") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "comment_reports" ADD CONSTRAINT "comment_reports_approvedBy_fkey" FOREIGN KEY ("approvedBy") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "comment_reports" ADD CONSTRAINT "comment_reports_approvedBy_fkey" FOREIGN KEY ("approvedBy") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "comment_reports" ADD CONSTRAINT "comment_reports_userReporter_fkey" FOREIGN KEY ("userReporter") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "languages" ADD CONSTRAINT "languages_craetedBy_fkey" FOREIGN KEY ("craetedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "languages" ADD CONSTRAINT "languages_craetedBy_fkey" FOREIGN KEY ("craetedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@ -850,6 +862,9 @@ ALTER TABLE "email_system_histories" ADD CONSTRAINT "email_system_histories_user
-- AddForeignKey -- AddForeignKey
ALTER TABLE "hero_banner" ADD CONSTRAINT "hero_banner_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "hero_banner" ADD CONSTRAINT "hero_banner_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "hero_banner" ADD CONSTRAINT "hero_banner_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "system_notifications" ADD CONSTRAINT "system_notifications_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "system_notifications" ADD CONSTRAINT "system_notifications_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@ -880,12 +895,6 @@ ALTER TABLE "_MediaCharacters" ADD CONSTRAINT "_MediaCharacters_A_fkey" FOREIGN
-- AddForeignKey -- AddForeignKey
ALTER TABLE "_MediaCharacters" ADD CONSTRAINT "_MediaCharacters_B_fkey" FOREIGN KEY ("B") REFERENCES "medias"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "_MediaCharacters" ADD CONSTRAINT "_MediaCharacters_B_fkey" FOREIGN KEY ("B") REFERENCES "medias"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_MediaCollections" ADD CONSTRAINT "_MediaCollections_A_fkey" FOREIGN KEY ("A") REFERENCES "collections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_MediaCollections" ADD CONSTRAINT "_MediaCollections_B_fkey" FOREIGN KEY ("B") REFERENCES "medias"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "_UserSelectedSharingCollention" ADD CONSTRAINT "_UserSelectedSharingCollention_A_fkey" FOREIGN KEY ("A") REFERENCES "collections"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "_UserSelectedSharingCollention" ADD CONSTRAINT "_UserSelectedSharingCollention_A_fkey" FOREIGN KEY ("A") REFERENCES "collections"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,23 +0,0 @@
/*
Warnings:
- You are about to drop the column `buttonContent` on the `hero_banner` table. All the data in the column will be lost.
- You are about to drop the column `buttonLink` on the `hero_banner` table. All the data in the column will be lost.
- You are about to drop the column `description` on the `hero_banner` table. All the data in the column will be lost.
- You are about to drop the column `isClickable` on the `hero_banner` table. All the data in the column will be lost.
- You are about to drop the column `tags` on the `hero_banner` table. All the data in the column will be lost.
- You are about to drop the column `title` on the `hero_banner` table. All the data in the column will be lost.
- Added the required column `mediaId` to the `hero_banner` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "hero_banner" DROP COLUMN "buttonContent",
DROP COLUMN "buttonLink",
DROP COLUMN "description",
DROP COLUMN "isClickable",
DROP COLUMN "tags",
DROP COLUMN "title",
ADD COLUMN "mediaId" UUID NOT NULL;
-- AddForeignKey
ALTER TABLE "hero_banner" ADD CONSTRAINT "hero_banner_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "medias"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,14 +0,0 @@
/*
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");

View File

@ -0,0 +1,14 @@
/*
Warnings:
- The values [Winter,Spring,Summer,Fall] on the enum `Season` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "Season_new" AS ENUM ('winter', 'spring', 'summer', 'fall');
ALTER TABLE "medias" ALTER COLUMN "season" TYPE "Season_new" USING ("season"::text::"Season_new");
ALTER TYPE "Season" RENAME TO "Season_old";
ALTER TYPE "Season_new" RENAME TO "Season";
DROP TYPE "public"."Season_old";
COMMIT;

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
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<typeof addItemToCollectionSchema.headers>;
}) => {
return returnWriteResponse(ctx.set, 200, "Item added to collection successfully" + ctx.headers.cookie);
};

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -1,9 +1,9 @@
import Elysia from "elysia"; import Elysia from "elysia";
import { addItemToCollectionController } from "./controllers/addItemToCollection.controller"; import { addItemToCollectionBySytemController } from "./controllers/addItemToCollectionBySytem.controller";
import { addItemToCollectionSchema } from "./schemas/addItemToCollection.schema"; 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( export const collectionModule = new Elysia({ prefix: "/collections", tags: ["Collections"] })
"/:name", .post("/sys", addItemToCollectionBySytemController, addItemToCollectionBySytemSchema)
addItemToCollectionController, .delete("/sys", removeItemFromCollectionBySytemController, removeItemFromCollectionBySytemSchema);
addItemToCollectionSchema,
);

View File

@ -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);
}
};

View File

@ -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");
}
};

View File

@ -1,14 +1,12 @@
import { t } from "elysia"; import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema"; import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const addItemToCollectionSchema = { export const addItemToCollectionBySytemSchema = {
headers: t.Object({ headers: t.Object({
cookie: t.String({ description: "Authentication token in cookie format, e.g., auth_token=your_jwt_token;" }), 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({ 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"] }), itemId: t.String({ description: "ID of the item to be added to the collection", examples: ["12345"] }),
}), }),
detail: { detail: {

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -1,17 +1,14 @@
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { getMediaIdFromSlugRepository } from "../../../media/repositories/GET/getMediaIdFromSlug.repository"; import { selectMediaIdFromSlugRepository } from "../../../media/repositories/SELECT/selectMediaIdFromSlug.repository";
import { GetEpisodeDetailsParams } from "../../controllers/getEpisodeDetails.controller"; import { GetEpisodeDetailsParams } from "../../controllers/getEpisodeDetails.controller";
import { getEpisodeDetailsRepository } from "../../repositories/GET/getEpisodeDetails.repository"; import { getEpisodeDetailsRepository } from "../../repositories/GET/getEpisodeDetails.repository";
export const getEpisodeDetailsService = async ( export const getEpisodeDetailsService = async (params: GetEpisodeDetailsParams) => {
params: GetEpisodeDetailsParams,
) => {
try { try {
if (!params.mediaSlug || !params.episode) if (!params.mediaSlug || !params.episode) throw new AppError(400, "Media slug and episode are required.");
throw new AppError(400, "Media slug and episode are required.");
const mediaId = await getMediaIdFromSlugRepository(params.mediaSlug); const mediaId = await selectMediaIdFromSlugRepository(params.mediaSlug);
if (!mediaId?.id) throw new AppError(404, "Media not found."); if (!mediaId?.id) throw new AppError(404, "Media not found.");
const result = await getEpisodeDetailsRepository({ const result = await getEpisodeDetailsRepository({

View File

@ -1,17 +1,16 @@
import { Context } from "elysia"; import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler"; import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnReadResponse } from "../../../helpers/callback/httpResponse"; import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { getActiveHeroBannerService } from "../services/getActiveHeroBanner.service"; import { getActiveHeroBannerService } from "../services/getActiveHeroBanner.service";
import { getActiveHeroBannerSchema } from "../schemas/getActiveHeroBanner.schema";
export const getActiveHeroBannerController = async (ctx: Context) => { export const getActiveHeroBannerController = async (ctx: {
set: Context["set"];
header: Static<typeof getActiveHeroBannerSchema.headers>;
}) => {
try { try {
const response = await getActiveHeroBannerService(); const response = await getActiveHeroBannerService({ cookie: ctx.header?.cookie });
return returnReadResponse( return returnReadResponse(ctx.set, 200, "Active hero banners fetched successfully", response);
ctx.set,
200,
"Active hero banners fetched successfully",
response,
);
} catch (error) { } catch (error) {
return mainErrorHandler(ctx.set, error); return mainErrorHandler(ctx.set, error);
} }

View File

@ -1,7 +1,7 @@
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { prisma } from "../../../../utils/databases/prisma/connection"; import { prisma } from "../../../../utils/databases/prisma/connection";
export const findAllActiveHeroBannerRepository = async () => { export const findAllActiveHeroBannerRepository = async (userId?: string) => {
try { try {
return await prisma.heroBanner.findMany({ return await prisma.heroBanner.findMany({
where: { where: {
@ -36,6 +36,17 @@ export const findAllActiveHeroBannerRepository = async () => {
name: true, name: true,
}, },
}, },
_count: {
select: {
inCollections: {
where: {
collection: {
ownerId: userId,
},
},
},
},
},
}, },
}, },
}, },

View File

@ -0,0 +1,8 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const getActiveHeroBannerSchema = {
headers: t.Object({
cookie: t.Optional(t.String()),
}),
} satisfies AppRouteSchema;

View File

@ -1,22 +1,25 @@
import { redisKey } from "../../../config/redis/key";
import { AppError } from "../../../helpers/error/instances/app"; import { AppError } from "../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { redis } from "../../../utils/databases/redis/connection"; import { tokenValidationService } from "../../auth/services/http/tokenValidation.service";
import { findSystemPreferenceService } from "../../systemPreference/services/internal/findSystemPreference.service"; import { findSystemPreferenceService } from "../../systemPreference/services/internal/findSystemPreference.service";
import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllActiveHeroBanner.repository"; import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllActiveHeroBanner.repository";
export const getActiveHeroBannerService = async () => { export const getActiveHeroBannerService = async ({ cookie }: { cookie?: string }) => {
try { try {
const userId = cookie ? (await tokenValidationService(cookie)).user.id : undefined;
// Check if Hero Banner is enabled in system preferences // Check if Hero Banner is enabled in system preferences
const isHeroBannerEnabled = await findSystemPreferenceService("HERO_BANNER_ENABLED", "boolean"); const isHeroBannerEnabled = await findSystemPreferenceService("HERO_BANNER_ENABLED", "boolean");
if (!isHeroBannerEnabled) throw new AppError(403, "Hero Banner is disabled"); if (!isHeroBannerEnabled) throw new AppError(403, "Hero Banner is disabled");
// Try to get active banners from Redis cache // Dont implement caching just yet; implement collection caching first, then implement banner caching.
const cachedBanners = await redis.get(`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`); // Please note that currently, a database query is still required to check the collections.
if (cachedBanners) return JSON.parse(cachedBanners); // // Try to get active banners from Redis cache
// const cachedBanners = await redis.get(`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`);
// if (cachedBanners) return JSON.parse(cachedBanners);
// If not in cache, fetch from database and cache the result // If not in cache, fetch from database and cache the result
const activeBanners = await findAllActiveHeroBannerRepository(); const activeBanners = await findAllActiveHeroBannerRepository(userId);
const constructedBanners = activeBanners.map((banner) => ({ const constructedBanners = activeBanners.map((banner) => ({
id: banner.media.id, id: banner.media.id,
title: banner.media.title, title: banner.media.title,
@ -27,11 +30,14 @@ export const getActiveHeroBannerService = async () => {
slug: genre.slug, slug: genre.slug,
name: genre.name, name: genre.name,
})), })),
isInCollection: banner.media._count.inCollections > 0,
})); }));
await redis.set(
`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`, // await redis.set(
JSON.stringify(constructedBanners), // `${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`,
); // JSON.stringify(constructedBanners),
// );
return constructedBanners; return constructedBanners;
} catch (error) { } catch (error) {
ErrorForwarder(error); ErrorForwarder(error);

View File

@ -1,27 +1,20 @@
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type"; import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type";
import { getMediaByMalIdRepository } from "../../../media/repositories/GET/getMediaByMalId.repository";
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { SystemAccountId } from "../../../../config/account/system"; import { SystemAccountId } from "../../../../config/account/system";
import { bulkInsertEpisodesRepository } from "../../repositories/bulkInsertEpisodes.repository"; import { bulkInsertEpisodesRepository } from "../../repositories/bulkInsertEpisodes.repository";
import { getEpisodeReferenceAPI } from "../../../../config/apis/jikan/episode.reference"; import { getEpisodeReferenceAPI } from "../../../../config/apis/jikan/episode.reference";
import { selectMediaByMalIdRepository } from "../../../media/repositories/SELECT/selectMediaByMalId.repository";
export const bulkInsertEpisodeService = async ( export const bulkInsertEpisodeService = async (mal_id: number, page: number = 1) => {
mal_id: number,
page: number = 1,
) => {
try { try {
const episodeAPI = getEpisodeReferenceAPI(mal_id); const episodeAPI = getEpisodeReferenceAPI(mal_id);
const episodeData: MediaEpisodeInfoResponse = await fetch( const episodeData: MediaEpisodeInfoResponse = await fetch(
`${episodeAPI.baseURL}${episodeAPI.getEpisodeList}?page=${page}`, `${episodeAPI.baseURL}${episodeAPI.getEpisodeList}?page=${page}`,
).then((res) => res.json()); ).then((res) => res.json());
const mediaData = await getMediaByMalIdRepository(mal_id); const mediaData = await selectMediaByMalIdRepository(mal_id);
if (!mediaData) if (!mediaData) throw new AppError(404, `Media with Mal ID ${mal_id} not found in database`);
throw new AppError(
404,
`Media with Mal ID ${mal_id} not found in database`,
);
const insertedEpisodeData = []; const insertedEpisodeData = [];
episodeData.data.forEach(async (episode) => { episodeData.data.forEach(async (episode) => {

View File

@ -1,19 +1,16 @@
import { Context } from "elysia"; import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler"; import { mainErrorHandler } from "../../../helpers/error/handler";
import { getAllMediaService } from "../services/http/getAllMedia.service"; import { getAllMediaService } from "../services/http/getAllMedia.service";
import { returnReadResponse } from "../../../helpers/callback/httpResponse"; import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { getAllMediaSchema } from "../schemas/getAllMedia.schema";
export const getAllMediaController = async ( export const getAllMediaController = async (ctx: {
ctx: Context & { query: { page: string } }, set: Context["set"];
) => { query: Static<typeof getAllMediaSchema.query>;
}) => {
try { try {
const mediaData = await getAllMediaService(ctx.query.page); const mediaData = await getAllMediaService(ctx.query.page);
return returnReadResponse( return returnReadResponse(ctx.set, 200, "Media fetched successfully", mediaData);
ctx.set,
200,
"Media fetched successfully",
mediaData,
);
} catch (error) { } catch (error) {
return mainErrorHandler(ctx.set, error); return mainErrorHandler(ctx.set, error);
} }

View File

@ -0,0 +1,17 @@
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { getMediaBySlugSchema } from "../schemas/getMediaBySlug.schema";
import { getMediaBySlugService } from "../services/http/getMediaBySlug.service";
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
export const getMediaBySlugController = async (ctx: {
set: Context["set"];
params: Static<typeof getMediaBySlugSchema.params>;
}) => {
try {
const mediaData = await getMediaBySlugService(ctx.params.slug);
return returnReadResponse(ctx.set, 200, "Media fetched successfully", mediaData);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,7 +1,9 @@
import Elysia from "elysia"; import Elysia from "elysia";
import { getAllMediaController } from "./controllers/getAllMedia.controller"; import { getAllMediaController } from "./controllers/getAllMedia.controller";
import { getMediaBySlugController } from "./controllers/getMediaBySlug.controller";
import { getMediaBySlugSchema } from "./schemas/getMediaBySlug.schema";
import { getAllMediaSchema } from "./schemas/getAllMedia.schema";
export const mediaModule = new Elysia({ prefix: "/media" }).get( export const mediaModule = new Elysia({ prefix: "/media", tags: ["Media"] })
"/", .get("/", getAllMediaController, getAllMediaSchema)
getAllMediaController, .get("/:slug", getMediaBySlugController, getMediaBySlugSchema);
);

View File

@ -1,7 +1,7 @@
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { mediaModel } from "../../model"; import { mediaModel } from "../../model";
export const getAllMediaRepository = async (page: number) => { export const selectAllMediaRepository = async (page: number) => {
try { try {
const limit = 10; const limit = 10;
return await mediaModel.findMany({ return await mediaModel.findMany({

View File

@ -1,7 +1,7 @@
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { mediaModel } from "../../model"; import { mediaModel } from "../../model";
export const getMediaByMalIdRepository = async (mal_id: number) => { export const selectMediaByMalIdRepository = async (mal_id: number) => {
try { try {
return await mediaModel.findUnique({ return await mediaModel.findUnique({
where: { malId: mal_id }, where: { malId: mal_id },

View File

@ -0,0 +1,12 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { prisma } from "../../../../utils/databases/prisma/connection";
export const selectMediaBySlugRepository = async (slug: string) => {
try {
return await prisma.media.findUnique({
where: { slug },
});
} catch (error) {
throw new AppError(500, "Failed to fetch media by slug", error);
}
};

View File

@ -1,7 +1,7 @@
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { mediaModel } from "../../model"; import { mediaModel } from "../../model";
export const getMediaIdFromSlugRepository = async (slug: string) => { export const selectMediaIdFromSlugRepository = async (slug: string) => {
try { try {
return await mediaModel.findUnique({ return await mediaModel.findUnique({
where: { slug }, where: { slug },

View File

@ -0,0 +1,58 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const getAllMediaSchema = {
query: t.Object({
page: t.String({ description: "The page number for pagination", default: "1" }),
}),
detail: {
summary: "Fetch all media items with pagination",
description:
"Fetch a paginated list of all media items. The 'page' query parameter can be used to specify the page number for pagination.",
responses: {
200: {
description: "Media items fetched successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: true },
status: { type: "number", example: 200 },
message: { type: "string", example: "Media fetched successfully" },
data: {
type: "array",
items: {
type: "object",
properties: {
status: { type: "string", example: "Finished Airing" },
id: { type: "string", example: "12345" },
title: { type: "string", example: "Example Media Title" },
slug: { type: "string", example: "example-media-title" },
malId: { type: "number", example: 67890 },
pictureMedium: { type: "string", example: "https://example.com/medium.jpg" },
pictureLarge: { type: "string", example: "https://example.com/large.jpg" },
country: { type: "string", example: "JP" },
score: { type: "number", example: 8.5 },
startAiring: { type: "string", format: "date-time", example: "2023-01-01T00:00:00Z" },
endAiring: { type: "string", format: "date-time", example: "2023-12-31T23:59:59Z" },
synopsis: { type: "string", example: "This is an example synopsis of the media item." },
ageRating: { type: "string", example: "PG-13" },
mediaType: { type: "string", example: "Anime" },
source: { type: "string", example: "Manga" },
onDraft: { type: "boolean", example: false },
uploadedBy: { type: "string", example: "admin" },
deletedAt: { type: "string", format: "date-time", nullable: true, example: null },
createdAt: { type: "string", format: "date-time", example: "2023-01-01T00:00:00Z" },
updatedAt: { type: "string", format: "date-time", example: "2023-01-02T00:00:00Z" },
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,29 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const getMediaBySlugSchema = {
params: t.Object({
slug: t.String({ description: "The slug of the media to fetch" }),
}),
detail: {
summary: "Fetch a media item by its slug",
description: "Fetch the specified media item using its slug. This endpoint returns the media details if found.",
responses: {
200: {
description: "Media item fetched successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: true },
status: { type: "number", example: 200 },
message: { type: "string", example: "Media fetched successfully" },
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -1,14 +1,11 @@
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { getAllMediaRepository } from "../../repositories/GET/getAllMedia.repository"; import { selectAllMediaRepository } from "../../repositories/SELECT/selectAllMedia.repository";
export const getAllMediaService = async (pagination: string) => { export const getAllMediaService = async (pagination: string) => {
try { try {
const page = const page = /^\d+$/.test(pagination) && Number(pagination) > 0 ? Number(pagination) : 1;
/^\d+$/.test(pagination) && Number(pagination) > 0
? Number(pagination)
: 1;
return getAllMediaRepository(page); return selectAllMediaRepository(page);
} catch (error) { } catch (error) {
ErrorForwarder(error); ErrorForwarder(error);
} }

View File

@ -0,0 +1,14 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { selectMediaBySlugRepository } from "../../repositories/SELECT/selectMediaBySlug.repository";
export const getMediaBySlugService = async (slug: string) => {
try {
const mediaData = await selectMediaBySlugRepository(slug);
if (!mediaData) throw new AppError(404, "Media not found with the provided slug.");
return mediaData;
} catch (error) {
ErrorForwarder(error);
}
};