From 5c7e82cd52501a636f148e93162e1ee66e27351a Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Thu, 29 Jan 2026 13:00:19 +0700 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20create=20bulk=20insert?= =?UTF-8?q?=20for=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(), + }, }, }, }); From 1b039a3d313e2d82e2d592c1d0bdc0fe0f919edd Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Thu, 29 Jan 2026 13:24:52 +0700 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9A=A1=20perf:=20optimize=20program=20st?= =?UTF-8?q?art-up=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.ts | 29 ++++++++++++++++++----------- src/utils/monitoring/sentry/init.ts | 20 ++++++++++++++------ src/utils/startups/validateEnv.ts | 9 ++++++--- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0994007..01a69cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,25 @@ import { middleware } from "./middleware"; import { validateEnv } from "./utils/startups/validateEnv"; + validateEnv(); -const { Elysia } = await import("elysia"); -const { routes } = await import("./routes"); +async function bootstrap() { + const { Elysia } = await import("elysia"); -const { sentryInit } = await import("./utils/monitoring/sentry/init"); -sentryInit(); + const { routes } = require("./routes"); + const { sentryInit } = require("./utils/monitoring/sentry/init"); -const app = new Elysia() - .use(middleware) - .use(routes) - .listen(process.env.APP_PORT || 3000); + sentryInit(); -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`, -); + console.log("\x1b[1m\x1b[33mπŸš€ Starting backend services...\x1b[0m"); + const app = new Elysia() + .use(middleware) + .use(routes) + .listen(process.env.APP_PORT || 3000); + + console.log( + `\x1b[1m\x1b[32mβœ… Backend service started on: ${process.env.APP_URL}\x1b[0m`, + ); +} + +bootstrap(); diff --git a/src/utils/monitoring/sentry/init.ts b/src/utils/monitoring/sentry/init.ts index 8848258..86266ca 100644 --- a/src/utils/monitoring/sentry/init.ts +++ b/src/utils/monitoring/sentry/init.ts @@ -1,8 +1,16 @@ import { init } from "@sentry/node"; -export const sentryInit = () => - init({ - dsn: process.env.SENTRY_DSN, - tracesSampleRate: 1.0, - environment: process.env.APP_ENV, - }); +export const sentryInit = () => { + console.log("πŸ”§ Initializing Sentry..."); + try { + init({ + dsn: process.env.SENTRY_DSN, + tracesSampleRate: 1.0, + environment: process.env.APP_ENV, + }); + console.log("βœ… Sentry initialized."); + } catch (error) { + console.error("❌ Failed to initialize Sentry:", error); + process.exit(1); + } +}; diff --git a/src/utils/startups/validateEnv.ts b/src/utils/startups/validateEnv.ts index 61ac380..24a2e45 100644 --- a/src/utils/startups/validateEnv.ts +++ b/src/utils/startups/validateEnv.ts @@ -1,18 +1,19 @@ import fs from "fs"; export const validateEnv = () => { + console.log("πŸ” Validating environment variables..."); if (!fs.existsSync(".env")) { if (fs.existsSync(".env.example")) { console.error("⚠️ .env file not found"); console.warn("πŸ“ Creating .env file from .env.example, please wait..."); fs.copyFileSync(".env.example", ".env"); console.warn( - "πŸ–ŠοΈ .env file successfully created please fill in the value in each key needed" + "πŸ–ŠοΈ .env file successfully created please fill in the value in each key needed", ); process.exit(1); } else { console.error( - `❌ Can't validate environment variable because can't find .env.example file. seems to be missing files please re-pull with β€œgit pull main”` + `❌ Can't validate environment variable because can't find .env.example file. seems to be missing files please re-pull with β€œgit pull main”`, ); process.exit(1); } @@ -25,7 +26,7 @@ export const validateEnv = () => { .filter((key) => key && !key.startsWith("#")); const missingKeys = exampleKeys.filter( - (key) => !process.env[key] || process.env[key].trim() === "" + (key) => !process.env[key] || process.env[key].trim() === "", ); if (missingKeys.length > 0) { @@ -34,4 +35,6 @@ export const validateEnv = () => { console.error(`check your .env file and make sure all keys are filled in`); process.exit(1); } + + console.log("βœ… Environment variables are valid."); };