Merge pull request #4 from rafiarrafif/chore/startup

Chore/startup
This commit is contained in:
Rafi Arrafif
2026-01-29 13:25:59 +07:00
committed by GitHub
17 changed files with 207 additions and 40 deletions

View File

@ -135,7 +135,8 @@ Table episodes {
mediaId String [not null] mediaId String [not null]
episode Int [not null] episode Int [not null]
name String [not null] name String [not null]
pictureThumbnail String [not null] score Decimal [not null, default: 0]
pictureThumbnail String
viewed BigInt [not null, default: 0] viewed BigInt [not null, default: 0]
likes BigInt [not null, default: 0] likes BigInt [not null, default: 0]
dislikes BigInt [not null, default: 0] dislikes BigInt [not null, default: 0]
@ -149,6 +150,10 @@ Table episodes {
videos videos [not null] videos videos [not null]
user_histories watch_histories [not null] user_histories watch_histories [not null]
comments comments [not null] comments comments [not null]
indexes {
(mediaId, episode) [unique]
}
} }
Table episode_likes { Table episode_likes {

View File

@ -171,7 +171,8 @@ model Episode {
mediaId String @db.Uuid mediaId String @db.Uuid
episode Int episode Int
name String @db.VarChar(255) 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) viewed BigInt @default(0)
likes BigInt @default(0) likes BigInt @default(0)
dislikes BigInt @default(0) dislikes BigInt @default(0)
@ -186,6 +187,8 @@ model Episode {
videos Video[] @relation("EpisodeVideos") videos Video[] @relation("EpisodeVideos")
user_histories WatchHistory[] @relation("EpisodeWatchHistories") user_histories WatchHistory[] @relation("EpisodeWatchHistories")
comments Comment[] @relation("EpisodeComments") comments Comment[] @relation("EpisodeComments")
@@unique([mediaId, episode])
@@map("episodes") @@map("episodes")
} }

View File

@ -0,0 +1,8 @@
import { baseURL } from "./baseUrl";
export const getEpisodeReferenceAPI = (malId: number) => {
return {
baseURL,
getEpisodeList: `/anime/${malId}/episodes`,
};
};

View File

@ -1,18 +1,25 @@
import { middleware } from "./middleware"; import { middleware } from "./middleware";
import { validateEnv } from "./utils/startups/validateEnv"; import { validateEnv } from "./utils/startups/validateEnv";
validateEnv(); validateEnv();
async function bootstrap() {
const { Elysia } = await import("elysia"); const { Elysia } = await import("elysia");
const { routes } = await import("./routes");
const { sentryInit } = await import("./utils/monitoring/sentry/init"); const { routes } = require("./routes");
const { sentryInit } = require("./utils/monitoring/sentry/init");
sentryInit(); sentryInit();
console.log("\x1b[1m\x1b[33m🚀 Starting backend services...\x1b[0m");
const app = new Elysia() const app = new Elysia()
.use(middleware) .use(middleware)
.use(routes) .use(routes)
.listen(process.env.APP_PORT || 3000); .listen(process.env.APP_PORT || 3000);
console.log( console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`, `\x1b[1m\x1b[32m✅ Backend service started on: ${process.env.APP_URL}\x1b[0m`,
); );
}
bootstrap();

View File

@ -1,6 +1,6 @@
import { Context } from "elysia"; import { Context } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler"; 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 { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { bulkInsertCharWithVAService } from "../services/internal/bulkInsertCharWithVA.service"; import { bulkInsertCharWithVAService } from "../services/internal/bulkInsertCharWithVA.service";

View File

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

View File

@ -1,7 +1,7 @@
import Elysia from "elysia"; import Elysia from "elysia";
import { bulkInsertAnimeController } from "./controllers/bulkInsertAnime.controller"; import { bulkInsertAnimeController } from "./controllers/bulkInsertAnime.controller";
import { bulkInsertEpisodeController } from "./controllers/bulkInsertEpisode.controller";
export const internalModule = new Elysia({ prefix: "/internal" }).post( export const internalModule = new Elysia({ prefix: "/internal" })
"/media/bulk-insert", .post("/media/bulk-insert", bulkInsertAnimeController)
bulkInsertAnimeController, .post("/episode/bulk-insert", bulkInsertEpisodeController);
);

View File

@ -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<Prisma.EpisodeUncheckedCreateInput, "id">,
) => {
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);
}
};

View File

@ -1,7 +1,6 @@
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
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";
import { MediaFullInfoResponse } from "../types/mediaFullInfo.type";
import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; import { generateUUIDv7 } from "../../../helpers/databases/uuidv7";
/** /**

View File

@ -1,14 +1,14 @@
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { getContentReferenceAPI } from "../../../config/apis/media.reference"; import { getContentReferenceAPI } from "../../../../config/apis/media.reference";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { bulkInsertGenresRepository } from "../repositories/bulkInsertGenres.repository"; import { bulkInsertGenresRepository } from "../../repositories/bulkInsertGenres.repository";
import { InsertMediaRepository } from "../repositories/bulkinsertMedia.repository"; import { InsertMediaRepository } from "../../repositories/bulkinsertMedia.repository";
import { bulkInsertStudiosRepository } from "../repositories/bulkInsertStudios.repository"; import { bulkInsertStudiosRepository } from "../../repositories/bulkInsertStudios.repository";
import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; import { MediaFullInfoResponse } from "../../types/mediaFullInfo.type";
import { generateSlug } from "../../../helpers/characters/generateSlug"; import { generateSlug } from "../../../../helpers/characters/generateSlug";
import { bulkInsertCharWithVAService } from "./internal/bulkInsertCharWithVA.service"; import { bulkInsertCharWithVAService } from "../internal/bulkInsertCharWithVA.service";
import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; import { generateUUIDv7 } from "../../../../helpers/databases/uuidv7";
import { SystemAccountId } from "../../../config/account/system"; import { SystemAccountId } from "../../../../config/account/system";
export const bulkInsertAnimeService = async (malId: number) => { export const bulkInsertAnimeService = async (malId: number) => {
try { try {

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import { prisma } from "../../utils/databases/prisma/connection";
export const mediaModel = prisma.media;

View File

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

View File

@ -1,16 +1,20 @@
import { generateUUIDv7 } from "../../../../helpers/databases/uuidv7";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { userModel } from "../../user.model"; import { userModel } from "../../user.model";
import { createUserViaRegisterInput } from "../../user.types"; import { createUserViaRegisterInput } from "../../user.types";
export const createUserViaRegisterRepository = async ( export const createUserViaRegisterRepository = async (
payload: createUserViaRegisterInput payload: createUserViaRegisterInput,
) => { ) => {
try { try {
return await userModel.create({ return await userModel.create({
data: { data: {
...payload, ...payload,
id: generateUUIDv7(),
preference: { preference: {
create: {}, create: {
id: generateUUIDv7(),
},
}, },
}, },
}); });

View File

@ -1,8 +1,16 @@
import { init } from "@sentry/node"; import { init } from "@sentry/node";
export const sentryInit = () => export const sentryInit = () => {
console.log("🔧 Initializing Sentry...");
try {
init({ init({
dsn: process.env.SENTRY_DSN, dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
environment: process.env.APP_ENV, environment: process.env.APP_ENV,
}); });
console.log("✅ Sentry initialized.");
} catch (error) {
console.error("❌ Failed to initialize Sentry:", error);
process.exit(1);
}
};

View File

@ -1,18 +1,19 @@
import fs from "fs"; import fs from "fs";
export const validateEnv = () => { export const validateEnv = () => {
console.log("🔍 Validating environment variables...");
if (!fs.existsSync(".env")) { if (!fs.existsSync(".env")) {
if (fs.existsSync(".env.example")) { if (fs.existsSync(".env.example")) {
console.error("⚠️ .env file not found"); console.error("⚠️ .env file not found");
console.warn("📝 Creating .env file from .env.example, please wait..."); console.warn("📝 Creating .env file from .env.example, please wait...");
fs.copyFileSync(".env.example", ".env"); fs.copyFileSync(".env.example", ".env");
console.warn( 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); process.exit(1);
} else { } else {
console.error( 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); process.exit(1);
} }
@ -25,7 +26,7 @@ export const validateEnv = () => {
.filter((key) => key && !key.startsWith("#")); .filter((key) => key && !key.startsWith("#"));
const missingKeys = exampleKeys.filter( const missingKeys = exampleKeys.filter(
(key) => !process.env[key] || process.env[key].trim() === "" (key) => !process.env[key] || process.env[key].trim() === "",
); );
if (missingKeys.length > 0) { 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`); console.error(`check your .env file and make sure all keys are filled in`);
process.exit(1); process.exit(1);
} }
console.log("✅ Environment variables are valid.");
}; };