From 5c7e82cd52501a636f148e93162e1ee66e27351a Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Thu, 29 Jan 2026 13:00:19 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20create=20bulk=20insert=20fo?= =?UTF-8?q?r=20episode=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/dbml/schema.dbml | 7 ++- prisma/schema.prisma | 5 ++- src/config/apis/episode.reference.ts | 8 ++++ .../controllers/bulkInsertAnime.controller.ts | 2 +- .../bulkInsertEpisode.controller.ts | 24 +++++++++++ src/modules/internal/index.ts | 8 ++-- .../bulkInsertEpisodes.repository.ts | 26 +++++++++++ .../bulkinsertMedia.repository.ts | 1 - .../{ => http}/bulkInsertAnime.service.ts | 20 ++++----- .../http/bulkInsertEpisode.service.ts | 43 +++++++++++++++++++ .../internal/types/mediaEpisodeInfo.type.ts | 22 ++++++++++ src/modules/media/model.ts | 3 ++ .../GET/getMediaByMalId.repository.ts | 12 ++++++ .../createUserViaRegister.repository.ts | 8 +++- 14 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 src/config/apis/episode.reference.ts create mode 100644 src/modules/internal/controllers/bulkInsertEpisode.controller.ts create mode 100644 src/modules/internal/repositories/bulkInsertEpisodes.repository.ts rename src/modules/internal/services/{ => http}/bulkInsertAnime.service.ts (70%) create mode 100644 src/modules/internal/services/http/bulkInsertEpisode.service.ts create mode 100644 src/modules/internal/types/mediaEpisodeInfo.type.ts create mode 100644 src/modules/media/model.ts create mode 100644 src/modules/media/repositories/GET/getMediaByMalId.repository.ts diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 226b24a..0619e0f 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -135,7 +135,8 @@ Table episodes { mediaId String [not null] episode Int [not null] name String [not null] - pictureThumbnail String [not null] + score Decimal [not null, default: 0] + pictureThumbnail String viewed BigInt [not null, default: 0] likes BigInt [not null, default: 0] dislikes BigInt [not null, default: 0] @@ -149,6 +150,10 @@ Table episodes { videos videos [not null] user_histories watch_histories [not null] comments comments [not null] + + indexes { + (mediaId, episode) [unique] + } } Table episode_likes { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ef3b20a..939f482 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -171,7 +171,8 @@ model Episode { mediaId String @db.Uuid episode Int name String @db.VarChar(255) - pictureThumbnail String @db.Text + score Decimal @db.Decimal(4,2) @default(0.00) + pictureThumbnail String? @db.Text viewed BigInt @default(0) likes BigInt @default(0) dislikes BigInt @default(0) @@ -186,6 +187,8 @@ model Episode { videos Video[] @relation("EpisodeVideos") user_histories WatchHistory[] @relation("EpisodeWatchHistories") comments Comment[] @relation("EpisodeComments") + + @@unique([mediaId, episode]) @@map("episodes") } diff --git a/src/config/apis/episode.reference.ts b/src/config/apis/episode.reference.ts new file mode 100644 index 0000000..eb50d77 --- /dev/null +++ b/src/config/apis/episode.reference.ts @@ -0,0 +1,8 @@ +import { baseURL } from "./baseUrl"; + +export const getEpisodeReferenceAPI = (malId: number) => { + return { + baseURL, + getEpisodeList: `/anime/${malId}/episodes`, + }; +}; diff --git a/src/modules/internal/controllers/bulkInsertAnime.controller.ts b/src/modules/internal/controllers/bulkInsertAnime.controller.ts index 831f553..6b06b0a 100644 --- a/src/modules/internal/controllers/bulkInsertAnime.controller.ts +++ b/src/modules/internal/controllers/bulkInsertAnime.controller.ts @@ -1,6 +1,6 @@ import { Context } from "elysia"; import { mainErrorHandler } from "../../../helpers/error/handler"; -import { bulkInsertAnimeService } from "../services/bulkInsertAnime.service"; +import { bulkInsertAnimeService } from "../services/http/bulkInsertAnime.service"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { bulkInsertCharWithVAService } from "../services/internal/bulkInsertCharWithVA.service"; diff --git a/src/modules/internal/controllers/bulkInsertEpisode.controller.ts b/src/modules/internal/controllers/bulkInsertEpisode.controller.ts new file mode 100644 index 0000000..c6bc1ab --- /dev/null +++ b/src/modules/internal/controllers/bulkInsertEpisode.controller.ts @@ -0,0 +1,24 @@ +import { Context } from "elysia"; +import { mainErrorHandler } from "../../../helpers/error/handler"; +import { bulkInsertEpisodeService } from "../services/http/bulkInsertEpisode.service"; +import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; + +// add pagination query +export const bulkInsertEpisodeController = async ( + ctx: Context & { body: { media_mal_id: number }; query: { page?: number } }, +) => { + try { + const bulkInsertResult = await bulkInsertEpisodeService( + ctx.body.media_mal_id, + ctx.query.page, + ); + return returnWriteResponse( + ctx.set, + 201, + "Success bulk insert for episode", + bulkInsertResult, + ); + } catch (err) { + return mainErrorHandler(ctx.set, err); + } +}; diff --git a/src/modules/internal/index.ts b/src/modules/internal/index.ts index 2406c4b..e7838c8 100644 --- a/src/modules/internal/index.ts +++ b/src/modules/internal/index.ts @@ -1,7 +1,7 @@ import Elysia from "elysia"; import { bulkInsertAnimeController } from "./controllers/bulkInsertAnime.controller"; +import { bulkInsertEpisodeController } from "./controllers/bulkInsertEpisode.controller"; -export const internalModule = new Elysia({ prefix: "/internal" }).post( - "/media/bulk-insert", - bulkInsertAnimeController, -); +export const internalModule = new Elysia({ prefix: "/internal" }) + .post("/media/bulk-insert", bulkInsertAnimeController) + .post("/episode/bulk-insert", bulkInsertEpisodeController); diff --git a/src/modules/internal/repositories/bulkInsertEpisodes.repository.ts b/src/modules/internal/repositories/bulkInsertEpisodes.repository.ts new file mode 100644 index 0000000..bfd7c58 --- /dev/null +++ b/src/modules/internal/repositories/bulkInsertEpisodes.repository.ts @@ -0,0 +1,26 @@ +import { Prisma } from "@prisma/client"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; +import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; + +export const bulkInsertEpisodesRepository = async ( + payload: Omit, +) => { + try { + return await prisma.episode.upsert({ + where: { + mediaId_episode: { + mediaId: payload.mediaId as string, + episode: payload.episode as number, + }, + }, + update: payload, + create: { + id: generateUUIDv7(), + ...payload, + }, + }); + } catch (err) { + throw new AppError(500, "Failed to bulk insert episodes", err); + } +}; diff --git a/src/modules/internal/repositories/bulkinsertMedia.repository.ts b/src/modules/internal/repositories/bulkinsertMedia.repository.ts index b1b3588..35b9063 100644 --- a/src/modules/internal/repositories/bulkinsertMedia.repository.ts +++ b/src/modules/internal/repositories/bulkinsertMedia.repository.ts @@ -1,7 +1,6 @@ import { Prisma } from "@prisma/client"; import { AppError } from "../../../helpers/error/instances/app"; import { prisma } from "../../../utils/databases/prisma/connection"; -import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; /** diff --git a/src/modules/internal/services/bulkInsertAnime.service.ts b/src/modules/internal/services/http/bulkInsertAnime.service.ts similarity index 70% rename from src/modules/internal/services/bulkInsertAnime.service.ts rename to src/modules/internal/services/http/bulkInsertAnime.service.ts index 2c3cef1..67b2a08 100644 --- a/src/modules/internal/services/bulkInsertAnime.service.ts +++ b/src/modules/internal/services/http/bulkInsertAnime.service.ts @@ -1,14 +1,14 @@ import { Prisma } from "@prisma/client"; -import { getContentReferenceAPI } from "../../../config/apis/media.reference"; -import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; -import { bulkInsertGenresRepository } from "../repositories/bulkInsertGenres.repository"; -import { InsertMediaRepository } from "../repositories/bulkinsertMedia.repository"; -import { bulkInsertStudiosRepository } from "../repositories/bulkInsertStudios.repository"; -import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; -import { generateSlug } from "../../../helpers/characters/generateSlug"; -import { bulkInsertCharWithVAService } from "./internal/bulkInsertCharWithVA.service"; -import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; -import { SystemAccountId } from "../../../config/account/system"; +import { getContentReferenceAPI } from "../../../../config/apis/media.reference"; +import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { bulkInsertGenresRepository } from "../../repositories/bulkInsertGenres.repository"; +import { InsertMediaRepository } from "../../repositories/bulkinsertMedia.repository"; +import { bulkInsertStudiosRepository } from "../../repositories/bulkInsertStudios.repository"; +import { MediaFullInfoResponse } from "../../types/mediaFullInfo.type"; +import { generateSlug } from "../../../../helpers/characters/generateSlug"; +import { bulkInsertCharWithVAService } from "../internal/bulkInsertCharWithVA.service"; +import { generateUUIDv7 } from "../../../../helpers/databases/uuidv7"; +import { SystemAccountId } from "../../../../config/account/system"; export const bulkInsertAnimeService = async (malId: number) => { try { diff --git a/src/modules/internal/services/http/bulkInsertEpisode.service.ts b/src/modules/internal/services/http/bulkInsertEpisode.service.ts new file mode 100644 index 0000000..a6aacd0 --- /dev/null +++ b/src/modules/internal/services/http/bulkInsertEpisode.service.ts @@ -0,0 +1,43 @@ +import { Prisma } from "@prisma/client"; +import { getEpisodeReferenceAPI } from "../../../../config/apis/episode.reference"; +import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type"; +import { getMediaByMalIdRepository } from "../../../media/repositories/GET/getMediaByMalId.repository"; +import { AppError } from "../../../../helpers/error/instances/app"; +import { SystemAccountId } from "../../../../config/account/system"; +import { bulkInsertEpisodesRepository } from "../../repositories/bulkInsertEpisodes.repository"; + +export const bulkInsertEpisodeService = async ( + mal_id: number, + page: number = 1, +) => { + try { + const episodeAPI = getEpisodeReferenceAPI(mal_id); + const episodeData: MediaEpisodeInfoResponse = await fetch( + `${episodeAPI.baseURL}${episodeAPI.getEpisodeList}?page=${page}`, + ).then((res) => res.json()); + + const mediaData = await getMediaByMalIdRepository(mal_id); + if (!mediaData) + throw new AppError( + 404, + `Media with Mal ID ${mal_id} not found in database`, + ); + + const insertedEpisodeData = []; + episodeData.data.forEach(async (episode) => { + insertedEpisodeData.push( + await bulkInsertEpisodesRepository({ + mediaId: mediaData.id!, + episode: episode.mal_id, + name: episode.title, + score: episode.score, + uploadedBy: SystemAccountId, + }), + ); + }); + return episodeData; + } catch (err) { + ErrorForwarder(err); + } +}; diff --git a/src/modules/internal/types/mediaEpisodeInfo.type.ts b/src/modules/internal/types/mediaEpisodeInfo.type.ts new file mode 100644 index 0000000..3b61990 --- /dev/null +++ b/src/modules/internal/types/mediaEpisodeInfo.type.ts @@ -0,0 +1,22 @@ +export interface MediaEpisodeInfoResponse { + pagination: Pagination; + data: Datum[]; +} + +export interface Datum { + mal_id: number; + url: string; + title: string; + title_japanese: string; + title_romanji: string; + aired: Date; + score: number; + filler: boolean; + recap: boolean; + forum_url: string; +} + +export interface Pagination { + last_visible_page: number; + has_next_page: boolean; +} diff --git a/src/modules/media/model.ts b/src/modules/media/model.ts new file mode 100644 index 0000000..3cc681e --- /dev/null +++ b/src/modules/media/model.ts @@ -0,0 +1,3 @@ +import { prisma } from "../../utils/databases/prisma/connection"; + +export const mediaModel = prisma.media; diff --git a/src/modules/media/repositories/GET/getMediaByMalId.repository.ts b/src/modules/media/repositories/GET/getMediaByMalId.repository.ts new file mode 100644 index 0000000..6d17559 --- /dev/null +++ b/src/modules/media/repositories/GET/getMediaByMalId.repository.ts @@ -0,0 +1,12 @@ +import { AppError } from "../../../../helpers/error/instances/app"; +import { mediaModel } from "../../model"; + +export const getMediaByMalIdRepository = async (mal_id: number) => { + try { + return await mediaModel.findUnique({ + where: { malId: mal_id }, + }); + } catch (err) { + throw new AppError(500, "Failed to get media by MAL ID", err); + } +}; diff --git a/src/modules/user/repositories/create/createUserViaRegister.repository.ts b/src/modules/user/repositories/create/createUserViaRegister.repository.ts index 1b85664..1c8c5b6 100644 --- a/src/modules/user/repositories/create/createUserViaRegister.repository.ts +++ b/src/modules/user/repositories/create/createUserViaRegister.repository.ts @@ -1,16 +1,20 @@ +import { generateUUIDv7 } from "../../../../helpers/databases/uuidv7"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { userModel } from "../../user.model"; import { createUserViaRegisterInput } from "../../user.types"; export const createUserViaRegisterRepository = async ( - payload: createUserViaRegisterInput + payload: createUserViaRegisterInput, ) => { try { return await userModel.create({ data: { ...payload, + id: generateUUIDv7(), preference: { - create: {}, + create: { + id: generateUUIDv7(), + }, }, }, });