@ -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 {
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
src/config/apis/episode.reference.ts
Normal file
8
src/config/apis/episode.reference.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { baseURL } from "./baseUrl";
|
||||||
|
|
||||||
|
export const getEpisodeReferenceAPI = (malId: number) => {
|
||||||
|
return {
|
||||||
|
baseURL,
|
||||||
|
getEpisodeList: `/anime/${malId}/episodes`,
|
||||||
|
};
|
||||||
|
};
|
||||||
13
src/index.ts
13
src/index.ts
@ -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();
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
);
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
22
src/modules/internal/types/mediaEpisodeInfo.type.ts
Normal file
22
src/modules/internal/types/mediaEpisodeInfo.type.ts
Normal 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;
|
||||||
|
}
|
||||||
3
src/modules/media/model.ts
Normal file
3
src/modules/media/model.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { prisma } from "../../utils/databases/prisma/connection";
|
||||||
|
|
||||||
|
export const mediaModel = prisma.media;
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -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.");
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user