Compare commits

..

23 Commits

Author SHA1 Message Date
dade012888 Merge pull request 'hotfix/payload-banner' (#28) from hotfix/payload-banner into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #28
2026-03-25 17:57:49 +07:00
4001aec6ef ♻️ refactor: refine payload before sending to frontend
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 47s
2026-03-25 17:55:48 +07:00
dd70f9f9d4 ♻️ refactor: restructure banner select payload 2026-03-25 17:49:16 +07:00
26909154ab Merge pull request 'prune-banner' (#27) from prune-banner into main
All checks were successful
Sync to GitHub / sync (push) Successful in 9s
Reviewed-on: #27
2026-03-25 12:52:55 +07:00
7f6b1373f4 💥 breaking: update endpoint to support new banner schema
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 1m42s
2026-03-25 12:39:23 +07:00
794a130562 feat: add cache flush module 2026-03-25 11:37:06 +07:00
6599fa8f79 🗃️ db: reset prisma migrations for updated banner structure 2026-03-17 16:41:29 +07:00
27b66e6d34 🗃️ db: update schema to match new banner logic 2026-03-16 22:45:13 +07:00
1c097aac69 Merge pull request 'refactor' (#26) from refactor into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #26
2026-03-11 10:12:24 +07:00
b5a0c2eda6 🚨 fix: resolve linting type error
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 33s
2026-03-11 10:11:09 +07:00
864a919680 📝 docs: complete documentation for auth module
Some checks failed
Integration Tests / integration-tests (pull_request) Failing after 42s
2026-03-11 10:07:33 +07:00
da74f5e3e1 📦 chore: snapshot commit before major changes 2026-03-11 09:24:25 +07:00
dc350d006b 📝 docs: add schema documentation to OAuth provider routes 2026-03-10 12:00:00 +07:00
97dc26ed82 👔 feat: add schema for token verification module 2026-03-09 12:00:00 +07:00
d767a0434c Merge pull request 'refactor' (#25) from refactor into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #25
2026-03-08 15:02:28 +07:00
43af43b0a3 📝 docs: finish controller documentation for internal module
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 51s
2026-03-08 14:59:27 +07:00
232ea986cb 🚨 fix: resolve linting type error 2026-03-08 14:53:26 +07:00
595a79de34 ♻️ refactor: add schema to all controllers in internal module 2026-03-08 14:50:54 +07:00
9f47f8f298 ♻️ refactor: align update-thumbnail and bulk-insert-video with latest Elysia standards 2026-03-08 07:31:38 +07:00
5a4e4d04a4 ♻️ refactor: align bulk-insert implementation with latest Elysia standards 2026-03-07 14:57:16 +07:00
0b786206e4 💥 breaking: upgrade Elysia to v1.4 and update codebase accordingly 2026-03-07 13:41:13 +07:00
c992314cf1 Merge pull request 'docs' (#24) from docs into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #24
2026-03-07 08:54:33 +07:00
d61b4f1d55 🚨 fix: resolve linting type error
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 32s
2026-03-07 08:53:45 +07:00
64 changed files with 1534 additions and 768 deletions

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"parser": "typescript",
"printWidth": 120
}

427
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@
"aws-sdk": "^2.1692.0",
"bcrypt": "^5.1.1",
"cookie": "^1.1.1",
"elysia": "latest",
"elysia": "^1.4.27",
"ioredis": "^5.6.1",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
@ -51,6 +51,7 @@
"cz-emoji": "^1.3.2-canary.2",
"eslint": "^9.29.0",
"globals": "^16.2.0",
"prettier": "^3.8.1",
"prisma": "^7.2.0",
"prisma-dbml-generator": "^0.12.0",
"typescript-eslint": "^8.34.1"

View File

@ -28,6 +28,7 @@ Table medias {
deletedAt DateTime
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
bannerPromotion hero_banner [not null]
logs media_logs [not null]
episodes episodes [not null]
collections collections [not null]
@ -496,12 +497,8 @@ Table email_system_histories {
Table hero_banner {
id String [pk]
orderPriority Int [unique]
isClickable Boolean [not null, default: false]
title String
tags String[] [not null]
description String
buttonContent String
buttonLink String
mediaId String [not null]
media medias [not null]
imageUrl String
startDate DateTime [not null]
endDate DateTime [not null]
@ -780,6 +777,8 @@ Ref: email_system_accounts.createdBy > users.id
Ref: email_system_histories.userRelated > users.id
Ref: hero_banner.mediaId > medias.id
Ref: hero_banner.creatorId > users.id
Ref: system_notifications.createdBy > users.id

View File

@ -206,7 +206,8 @@ CREATE TABLE "videos" (
"id" UUID NOT NULL,
"episodeId" UUID NOT NULL,
"serviceId" UUID NOT NULL,
"code" VARCHAR(255) NOT NULL,
"videoCode" VARCHAR(255) NOT NULL,
"thumbnailCode" TEXT,
"pendingUpload" BOOLEAN NOT NULL DEFAULT true,
"uploadedBy" UUID NOT NULL,
"deletedAt" TIMESTAMP(3),
@ -497,6 +498,26 @@ CREATE TABLE "email_system_histories" (
CONSTRAINT "email_system_histories_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "hero_banner" (
"id" UUID NOT NULL,
"orderPriority" INTEGER,
"isClickable" BOOLEAN NOT NULL DEFAULT false,
"title" VARCHAR(225),
"tags" TEXT[],
"description" TEXT,
"buttonContent" VARCHAR(100),
"buttonLink" TEXT,
"imageUrl" TEXT,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creatorId" UUID NOT NULL,
CONSTRAINT "hero_banner_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "system_preferences" (
"id" UUID NOT NULL,
@ -629,7 +650,7 @@ CREATE UNIQUE INDEX "lang_va_char_language_vaId_charId_key" ON "lang_va_char"("l
CREATE UNIQUE INDEX "episodes_mediaId_episode_key" ON "episodes"("mediaId", "episode");
-- CreateIndex
CREATE UNIQUE INDEX "videos_serviceId_code_key" ON "videos"("serviceId", "code");
CREATE UNIQUE INDEX "videos_serviceId_videoCode_key" ON "videos"("serviceId", "videoCode");
-- CreateIndex
CREATE UNIQUE INDEX "video_services_name_key" ON "video_services"("name");
@ -664,6 +685,12 @@ CREATE UNIQUE INDEX "email_system_accounts_email_key" ON "email_system_accounts"
-- CreateIndex
CREATE UNIQUE INDEX "email_system_accounts_username_key" ON "email_system_accounts"("username");
-- CreateIndex
CREATE UNIQUE INDEX "hero_banner_orderPriority_key" ON "hero_banner"("orderPriority");
-- CreateIndex
CREATE UNIQUE INDEX "system_preferences_key_key" ON "system_preferences"("key");
-- CreateIndex
CREATE INDEX "_MediaStudios_B_index" ON "_MediaStudios"("B");
@ -820,6 +847,9 @@ ALTER TABLE "email_system_accounts" ADD CONSTRAINT "email_system_accounts_create
-- AddForeignKey
ALTER TABLE "email_system_histories" ADD CONSTRAINT "email_system_histories_userRelated_fkey" FOREIGN KEY ("userRelated") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
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 "system_notifications" ADD CONSTRAINT "system_notifications_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,17 +0,0 @@
/*
Warnings:
- You are about to drop the column `code` on the `videos` table. All the data in the column will be lost.
- A unique constraint covering the columns `[serviceId,videoCode]` on the table `videos` will be added. If there are existing duplicate values, this will fail.
- Added the required column `videoCode` to the `videos` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "videos_serviceId_code_key";
-- AlterTable
ALTER TABLE "videos" RENAME COLUMN "code" TO "videoCode";
-- CreateIndex
DROP INDEX IF EXISTS "videos_serviceId_code_key";
CREATE UNIQUE INDEX "videos_serviceId_videoCode_key" ON "videos"("serviceId", "videoCode");

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "videos" ADD COLUMN "thumbnailCode" TEXT;

View File

@ -1,20 +0,0 @@
-- CreateTable
CREATE TABLE "HeroBanner" (
"id" UUID NOT NULL,
"isClickable" BOOLEAN NOT NULL DEFAULT false,
"title" VARCHAR(225),
"description" TEXT,
"buttonContent" VARCHAR(100),
"buttonLink" TEXT,
"imageUrl" TEXT,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creatorId" UUID NOT NULL,
CONSTRAINT "HeroBanner_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "HeroBanner" ADD CONSTRAINT "HeroBanner_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `HeroBanner` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "HeroBanner" DROP CONSTRAINT "HeroBanner_creatorId_fkey";
-- DropTable
DROP TABLE "HeroBanner";
-- CreateTable
CREATE TABLE "hero_banner" (
"id" UUID NOT NULL,
"isClickable" BOOLEAN NOT NULL DEFAULT false,
"title" VARCHAR(225),
"description" TEXT,
"buttonContent" VARCHAR(100),
"buttonLink" TEXT,
"imageUrl" TEXT,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creatorId" UUID NOT NULL,
CONSTRAINT "hero_banner_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "hero_banner" ADD CONSTRAINT "hero_banner_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,11 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[order]` on the table `hero_banner` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "hero_banner" ADD COLUMN "order" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "hero_banner_order_key" ON "hero_banner"("order");

View File

@ -1,16 +0,0 @@
/*
Warnings:
- You are about to drop the column `order` on the `hero_banner` table. All the data in the column will be lost.
- A unique constraint covering the columns `[orderPriority]` on the table `hero_banner` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "hero_banner_order_key";
-- AlterTable
ALTER TABLE "hero_banner" DROP COLUMN "order",
ADD COLUMN "orderPriority" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "hero_banner_orderPriority_key" ON "hero_banner"("orderPriority");

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "hero_banner" ADD COLUMN "tags" TEXT[];

View File

@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[key]` on the table `system_preferences` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "system_preferences_key_key" ON "system_preferences"("key");

View File

@ -0,0 +1,23 @@
/*
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

@ -49,6 +49,7 @@ model Media {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
bannerPromotion HeroBanner[] @relation("MediaBannerPromotion")
logs MediaLog[] @relation("MediaLogs")
episodes Episode[] @relation("MediaEpisodes")
collections Collection[] @relation("MediaCollections")
@ -557,12 +558,8 @@ model EmailSystemHistory {
model HeroBanner {
id String @id @db.Uuid
orderPriority Int? @unique
isClickable Boolean @default(false)
title String? @db.VarChar(225)
tags String[]
description String? @db.Text
buttonContent String? @db.VarChar(100)
buttonLink String? @db.Text
mediaId String @db.Uuid
media Media @relation("MediaBannerPromotion", fields: [mediaId], references: [id])
imageUrl String? @db.Text
startDate DateTime
endDate DateTime

View File

@ -0,0 +1,18 @@
import { ElysiaOpenAPIConfig } from "@elysiajs/openapi";
export const openAPIConfig: ElysiaOpenAPIConfig = {
documentation: {
info: {
title: "TV Nounoz API",
description: "API documentation for TV Nounoz backend services",
version: "1.0.0",
},
tags: [
{
name: "Internal",
description:
"Endpoints for internal use, such as administrative tasks and data management operations. These endpoints may require authentication and are not intended for public use.",
},
],
},
};

View File

@ -1,4 +1,3 @@
import { Context } from "elysia";
import { UserHeaderInformation } from "./types";
export interface ClientInfoHeader {
@ -10,25 +9,14 @@ export interface ClientInfoHeader {
ip: string;
}
export const getUserHeaderInformation = (
ctx: Context,
): UserHeaderInformation => {
const clientInfoHeader =
(JSON.parse(
ctx.request.headers.get("x-client-info") as string,
) as ClientInfoHeader) ?? ("unknown" as string);
export const getUserHeaderInformation = (clientInfo: string): UserHeaderInformation => {
const clientInfoHeader = (JSON.parse(clientInfo) as ClientInfoHeader) ?? ("unknown" as string);
const userHeaderInformation = {
ip: clientInfoHeader.ip ?? "unknown",
deviceType: clientInfoHeader.deviceType ?? "unknown",
deviceOS:
(clientInfoHeader.os ?? "unknown") +
" " +
(clientInfoHeader.osVersion ?? "unknown"),
browser:
(clientInfoHeader.browser ?? "unknown") +
" " +
(clientInfoHeader.browserVersion ?? "unknown"),
deviceOS: (clientInfoHeader.os ?? "unknown") + " " + (clientInfoHeader.osVersion ?? "unknown"),
browser: (clientInfoHeader.browser ?? "unknown") + " " + (clientInfoHeader.browserVersion ?? "unknown"),
};
return userHeaderInformation;

View File

@ -1,4 +1,4 @@
import { t, type RouteSchema } from "elysia";
import { type RouteSchema } from "elysia";
import type { OpenAPIV3 } from "openapi-types";
export type AppRouteSchema = RouteSchema & {
@ -12,9 +12,7 @@ export type AppRouteSchema = RouteSchema & {
requestBody?: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject;
responses: OpenAPIV3.ResponsesObject;
callbacks?: {
[callback: string]:
| OpenAPIV3.ReferenceObject
| OpenAPIV3.CallbackObject;
[callback: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.CallbackObject;
};
deprecated?: boolean;
security?: OpenAPIV3.SecurityRequirementObject[];

View File

@ -3,6 +3,7 @@
import openapi from "@elysiajs/openapi";
import { middleware } from "./middleware";
import { validateEnv } from "./utils/startups/validateEnv";
import { openAPIConfig } from "./config/documentation/openAPI";
validateEnv();
@ -18,19 +19,7 @@ async function bootstrap() {
new Elysia()
.use(middleware)
.use(routes)
.use(
openapi({
documentation: {
tags: [
{
name: "Internal",
description:
"Endpoints for internal use only, not exposed to public API consumers.",
},
],
},
}),
)
.use(openapi(openAPIConfig))
.listen(process.env.APP_PORT || 3000);
console.log(

View File

@ -9,8 +9,8 @@ export const getOauthProvidersController = (ctx: Context) => {
return returnReadResponse(
ctx.set,
200,
"Getting all oauth available list",
oauthProviderServices
"Successfully retrieved the list of oauth providers",
oauthProviderServices,
);
} catch (error) {
return mainErrorHandler(ctx.set, error);

View File

@ -1,14 +1,17 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { githubCallbackService } from "../services/http/githubCallback.service";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { getUserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation";
import { githubCallbackSchema } from "../schemas/githubCallback.schema";
export const githubCallbackController = async (
ctx: Context & { query: { code: string; callbackURI: string } }
) => {
export const githubCallbackController = async (ctx: {
set: Context["set"];
query: Static<typeof githubCallbackSchema.query>;
headers: Static<typeof githubCallbackSchema.headers>;
}) => {
try {
const userHeaderInfo = getUserHeaderInformation(ctx);
const userHeaderInfo = getUserHeaderInformation(ctx.headers["x-client-info"]);
const authToken = await githubCallbackService(ctx.query, userHeaderInfo);
return returnWriteResponse(ctx.set, 200, "Authenticated successfully!", {

View File

@ -1,21 +1,18 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { githubRequestService } from "../services/http/githubRequest.service";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { githubRequestSchema } from "../schemas/githubRequest.schema";
export const githubRequestController = async (
ctx: Context & { query: { callback?: string } },
) => {
export const githubRequestController = async (ctx: {
set: Context["set"];
query: Static<typeof githubRequestSchema.query>;
}) => {
try {
const loginUrl = await githubRequestService(ctx.query.callback);
return returnReadResponse(
ctx.set,
200,
"Login URL generated successfully",
{
return returnReadResponse(ctx.set, 200, "GitHub login URL created successfully.", {
endpointUrl: loginUrl,
},
);
});
} catch (error) {
return mainErrorHandler(ctx.set, error);
}

View File

@ -1,17 +1,20 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { googleCallbackService } from "../services/http/googleCallback.service";
import { getUserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation";
import { googleCallbackSchema } from "../schemas/googleCallback.schema";
export const googleCallbackController = async (
ctx: Context & { query: { code: string; state: string; callbackURI: string } }
) => {
export const googleCallbackController = async (ctx: {
set: Context["set"];
query: Static<typeof googleCallbackSchema.query>;
headers: Static<typeof googleCallbackSchema.headers>;
}) => {
try {
const userHeaderInfo = getUserHeaderInformation(ctx);
const userHeaderInfo = getUserHeaderInformation(ctx.headers["x-client-info"]);
const authToken = await googleCallbackService(ctx.query, userHeaderInfo);
return returnReadResponse(ctx.set, 200, "Authenticated successfully!", {
return returnReadResponse(ctx.set, 200, "Authentication successful!", {
authToken,
});
} catch (error) {

View File

@ -1,14 +1,16 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { googleRequestService } from "../services/http/googleRequest.service";
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { googleRequestSchema } from "../schemas/googleRequest.schema";
export const googleRequestController = async (
ctx: Context & { query: { callback?: string } }
) => {
export const googleRequestController = async (ctx: {
set: Context["set"];
query: Static<typeof googleRequestSchema.query>;
}) => {
try {
const loginUrl = await googleRequestService(ctx.query.callback);
return returnReadResponse(ctx.set, 200, "Google login url created!", {
return returnReadResponse(ctx.set, 200, "Google login URL created successfully.", {
endpointUrl: loginUrl,
});
} catch (error) {

View File

@ -7,13 +7,21 @@ import { getOauthProvidersController } from "./controllers/getOauthProviders.con
import { getCallbackProviderUrlController } from "./controllers/getCallbackProviderUrl.controller";
import { tokenValidationController } from "./controllers/tokenValidation.controller";
import { logoutController } from "./controllers/logout.controller";
import { tokenValidationSchema } from "./schemas/tokenValidation.schema";
import { getOauthProvidersSchema } from "./schemas/getOauthProviders.schema";
import { getCallbackProviderUrlSchema } from "./schemas/getCallbackProviderUrl.schema";
import { googleRequestSchema } from "./schemas/googleRequest.schema";
import { googleCallbackSchema } from "./schemas/googleCallback.schema";
import { githubRequestSchema } from "./schemas/githubRequest.schema";
import { githubCallbackSchema } from "./schemas/githubCallback.schema";
import { logoutSchema } from "./schemas/logout.schema";
export const authModule = new Elysia({ prefix: "/auth" })
.post("/token/validate", tokenValidationController)
.get("/providers", getOauthProvidersController)
.get("/providers/:name/callback", getCallbackProviderUrlController)
.get("/github", githubRequestController)
.get("/github/callback", githubCallbackController)
.get("/google", googleRequestController)
.get("/google/callback", googleCallbackController)
.post("/logout", logoutController);
export const authModule = new Elysia({ prefix: "/auth", tags: ["Authentication"] })
.post("/token/validate", tokenValidationController, tokenValidationSchema)
.get("/providers", getOauthProvidersController, getOauthProvidersSchema)
.get("/providers/:name/callback", getCallbackProviderUrlController, getCallbackProviderUrlSchema)
.get("/google", googleRequestController, googleRequestSchema)
.get("/google/callback", googleCallbackController, googleCallbackSchema)
.get("/github", githubRequestController, githubRequestSchema)
.get("/github/callback", githubCallbackController, githubCallbackSchema)
.post("/logout", logoutController, logoutSchema);

View File

@ -0,0 +1,45 @@
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const getCallbackProviderUrlSchema = {
detail: {
summary: "Get the callback URL of oauth provider",
description:
"After users have successfully completed the authentication process on the OAuth provider page, they will be redirected to the callback page on the frontend. This endpoint aims to obtain the actual endpoint for each OAuth response handler.",
responses: {
200: {
description: "The callback URL on the provider has been found.",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
default: true,
},
status: {
type: "number",
default: 200,
},
message: {
type: "string",
default: "The callback URL on the provider has been found.",
},
data: {
type: "object",
properties: {
callback_url: {
type: "string",
description: "The callback URL on the provider.",
example: "auth/google/callback",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,55 @@
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const getOauthProvidersSchema = {
detail: {
summary: "Get all available oauth providers",
description:
"This endpoint returns a list of all available and active oauth providers that can be used for authentication.",
responses: {
200: {
description: "Successfully retrieved the list of oauth providers",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
example: true,
},
status: {
type: "number",
example: 200,
},
message: {
type: "string",
example: "Successfully retrieved the list of oauth providers",
},
data: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
example: "google",
},
icon: {
type: "string",
example: "logos:google-icon",
},
req_endpoint: {
type: "string",
example: "auth/google",
},
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,57 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const githubCallbackSchema = {
headers: t.Object({
"x-client-info": t.String({
examples: [
'{"os":"Windows","osVersion":"10","browser":"Chrome","browserVersion":"89.0.4389.82","deviceType":"Desktop","ip":"192.168.1.1"}',
],
}),
}),
query: t.Object({
code: t.String({ examples: ["4/0AY0e-xxxxxxxxx"] }),
callbackURI: t.String({ examples: ["https://example.com/auth/github/callback"] }),
}),
detail: {
summary: "GitHub OAuth callback endpoint",
description:
"Handles the callback from GitHub OAuth and processes the authentication response. This endpoint also processes the account provisioning if the user is logging in for the first time.",
responses: {
200: {
description: "Authentication successful",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
example: true,
},
status: {
type: "number",
example: 200,
},
message: {
type: "string",
example: "Authentication successful",
},
data: {
type: "object",
properties: {
authToken: {
type: "string",
description: "JWT token for authenticated user",
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,54 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const githubRequestSchema = {
query: t.Object({
callback: t.Optional(
t.String({
description: "The callback URL to redirect after GitHub authentication. It should be URL-encoded if provided.",
}),
),
}),
detail: {
summary: "Initiate GitHub OAuth flow",
description:
"This endpoint initiates the GitHub OAuth flow by redirecting the user to GitHub's authentication page.",
responses: {
200: {
description: "GitHub login URL created successfully.",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
default: true,
},
status: {
type: "number",
default: 200,
},
message: {
type: "string",
default: "GitHub login URL created successfully.",
},
data: {
type: "object",
properties: {
endpointUrl: {
type: "string",
description: "The URL to redirect the user for GitHub authentication.",
example:
"https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=user:email",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,58 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const googleCallbackSchema = {
headers: t.Object({
"x-client-info": t.String({
examples: [
'{"os":"Windows","osVersion":"10","browser":"Chrome","browserVersion":"89.0.4389.82","deviceType":"Desktop","ip":"192.168.1.1"}',
],
}),
}),
query: t.Object({
code: t.String({ examples: ["4/0AY0e-xxxxxxxxx"] }),
state: t.String({ examples: ["random_state_string"] }),
callbackURI: t.String({ examples: ["https://example.com/auth/google/callback"] }),
}),
detail: {
summary: "Google OAuth callback endpoint",
description:
"Handles the callback from Google OAuth and processes the authentication response. This endpoint also processes the account provisioning if the user is logging in for the first time.",
responses: {
200: {
description: "Authentication successful",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
example: true,
},
status: {
type: "number",
example: 200,
},
message: {
type: "string",
example: "Authentication successful",
},
data: {
type: "object",
properties: {
authToken: {
type: "string",
description: "JWT token for authenticated user",
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,54 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const googleRequestSchema = {
query: t.Object({
callback: t.Optional(
t.String({
description: "The callback URL to redirect after Google authentication. It should be URL-encoded if provided.",
}),
),
}),
detail: {
summary: "Initiate Google OAuth flow",
description:
"This endpoint initiates the Google OAuth flow by redirecting the user to Google's authentication page.",
responses: {
200: {
description: "Google login URL created successfully.",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
default: true,
},
status: {
type: "number",
default: 200,
},
message: {
type: "string",
default: "Google login URL created successfully.",
},
data: {
type: "object",
properties: {
endpointUrl: {
type: "string",
description: "The URL to redirect the user for Google authentication.",
example:
"https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=email%20profile",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,97 @@
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const logoutSchema = {
detail: {
summary: "Logout endpoint",
description: "Logs out the authenticated user by invalidating their session or token.",
responses: {
200: {
description: "Logout successful",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
example: true,
},
status: {
type: "number",
example: 200,
},
message: {
type: "string",
example: "Logout successful",
},
data: {
type: "object",
description: "Details about the logout operation. This only returned in development environment.",
properties: {
id: {
type: "string",
example: "123e4567-e89b-12d3-a456-426614174000",
},
isAuthenticated: {
type: "boolean",
example: false,
},
validUntil: {
type: "string",
format: "date-time",
example: "2024-12-31T23:59:59Z",
},
userId: {
type: "string",
example: "user_12345",
},
deletedAt: {
type: "string",
format: "date-time",
example: "2024-01-02T12:00:00Z",
},
createdAt: {
type: "string",
format: "date-time",
example: "2024-01-01T12:00:00Z",
},
updatedAt: {
type: "string",
format: "date-time",
example: "2024-01-02T12:00:00Z",
},
deviceType: {
type: "string",
example: "Desktop",
},
deviceOs: {
type: "string",
example: "Windows 10",
},
deviceIp: {
type: "string",
example: "192.168.1.1",
},
browser: {
type: "string",
example: "Chrome 89.0.4389.82",
},
isOnline: {
type: "boolean",
example: false,
},
lastOnline: {
type: "string",
format: "date-time",
example: "2024-01-02T12:00:00Z",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,108 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const tokenValidationSchema = {
headers: t.Object({
cookie: t.String({ description: "Authentication token in cookie format, e.g., auth_token=your_jwt_token;" }),
}),
detail: {
summary: "Validate authentication JWT token",
description:
"Validates the provided authentication JWT token with checking its validity and expiration in redis cache, if not exists, it will be checked in the database. If the token is valid, it returns the user information associated with the token. if the token is invalid or expired, it returns an error message.",
responses: {
200: {
description: "Validation successful",
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: { type: "string", description: "Session ID", default: "xxxx-xxxxx-xxxxx-xxxx" },
isAuthenticated: {
type: "boolean",
description: "Indicates if the token is valid and the user is authenticated",
default: true,
},
validUntil: {
type: "string",
format: "date-time",
description: "Expiration date and time of the token",
default: "2024-12-31T23:59:59Z",
},
user: {
type: "object",
properties: {
id: { type: "string", description: "User ID", default: "user-12345" },
name: { type: "string", description: "User's full name", default: "Lena Nouzen" },
email: {
type: "string",
format: "email",
description: "User's email address",
default: "lena@example.com",
},
username: { type: "string", description: "User's username", default: "vladilena" },
avatar: {
type: "string",
format: "uri",
description: "URL to the user's avatar image",
default: "https://example.com/avatar.jpg",
},
birthDate: {
type: "string",
format: "date",
description: "User's birth date, can be null if not provided",
default: null,
},
bioProfile: {
type: "string",
description: "User's bio/profile description, can be null if not provided",
default: null,
},
preference: {
type: "object",
properties: {
id: { type: "string", description: "Preference ID", default: "pref-12345" },
userId: { type: "string", description: "Associated User ID", default: "user-12345" },
langPreference: {
type: "string",
description: "User's language preference, can be null if not provided",
default: null,
},
adultFiltering: {
type: "string",
description: "User's adult content filtering setting",
default: "strict",
},
adultAlert: {
type: "string",
description: "User's adult content alert setting",
default: "enabled",
},
videoQuality: {
type: "string",
description: "User's preferred video quality setting",
default: "1080p",
},
serviceDefaultId: {
type: "string",
description: "Default service ID for the user, can be null if not provided",
default: null,
},
hideContries: {
type: "array",
items: { type: "string" },
description: "List of country codes that the user has chosen to hide content from",
default: ["US", "CN"],
},
},
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,8 @@
import { Context } from "elysia";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { clearHeroBannerService } from "../services/clearHeroBanner.service";
export const clearHeroBannerController = async (ctx: { set: Context["set"] }) => {
const cacheCleared = await clearHeroBannerService();
return returnWriteResponse(ctx.set, 200, "Hero banner cache flushed successfully", cacheCleared);
};

View File

@ -0,0 +1,4 @@
import Elysia from "elysia";
import { clearHeroBannerController } from "./controllers/clearHeroBanner.controller";
export const flushCacheModule = new Elysia({ prefix: "/flush-cache" }).put("/hero-banner", clearHeroBannerController);

View File

@ -0,0 +1,12 @@
import { redisKey } from "../../../config/redis/key";
import { AppError } from "../../../helpers/error/instances/app";
import { redis } from "../../../utils/databases/redis/connection";
export const clearHeroBannerService = async () => {
try {
const cache = await redis.del(redisKey.find((key) => key.name === "HERO_BANNER")?.key || "");
return cache > 0; // Returns true if cache was cleared, false if it was not found
} catch (error) {
throw new AppError(500, "Failed to clear hero banner cache", error);
}
};

View File

@ -20,6 +20,25 @@ export const findAllActiveHeroBannerRepository = async () => {
startDate: "asc",
},
],
select: {
orderPriority: true,
imageUrl: true,
media: {
select: {
id: true,
title: true,
slug: true,
pictureLarge: true,
synopsis: true,
genres: {
select: {
slug: true,
name: true,
},
},
},
},
},
});
} catch (error) {
throw new AppError(500, "Failed to fetch active hero banners", error);

View File

@ -8,26 +8,31 @@ import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllAc
export const getActiveHeroBannerService = async () => {
try {
// Check if Hero Banner is enabled in system preferences
const isHeroBannerEnabled = await findSystemPreferenceService(
"HERO_BANNER_ENABLED",
"boolean",
);
if (!isHeroBannerEnabled)
throw new AppError(403, "Hero Banner is disabled");
const isHeroBannerEnabled = await findSystemPreferenceService("HERO_BANNER_ENABLED", "boolean");
if (!isHeroBannerEnabled) throw new AppError(403, "Hero Banner is disabled");
// Try to get active banners from Redis cache
const cachedBanners = await redis.get(
`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`,
);
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
const activeBanners = await findAllActiveHeroBannerRepository();
const constructedBanners = activeBanners.map((banner) => ({
id: banner.media.id,
title: banner.media.title,
slug: banner.media.slug,
imageUrl: banner.imageUrl || banner.media.pictureLarge,
synopsis: banner.media.synopsis,
genres: banner.media.genres.map((genre) => ({
slug: genre.slug,
name: genre.name,
})),
}));
await redis.set(
`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`,
JSON.stringify(activeBanners),
JSON.stringify(constructedBanners),
);
return activeBanners;
return constructedBanners;
} catch (error) {
ErrorForwarder(error);
}

View File

@ -1,62 +1,23 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { bulkInsertEpisodeService } from "../services/http/bulkInsertEpisode.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { bulkInsertEpisodeSchema } from "../schemas/bulkInsertEpisode.schema";
/**
* @function bulkInsertMediaController
* @description Perform bulk insert of episodes for a specific media. This operation fetches episode data from external sources and inserts them into the database. The page parameter is optional; if not provided, the first page of episodes will be fetched.
* Perform bulk insert of episodes for a specific media.
* This operation fetches episode data from external sources and inserts them into the database.
*
* @param {Context & { body: { media_mal_id: number }; query: { page?: number } }} ctx
* The context object containing the request body.
* The body must include:
* - media_mal_id: number - The MyAnimeList ID of the media for which episodes will be inserted.
* The query may include:
* - page?: number - (Optional) The page number of episodes to fetch and insert. If not provided, defaults to the first page.
*
* @example
* Request route: POST /internal/episode/bulk-insert
* Request body:
* {
* "media_mal_id": 12345
* }
* Query parameter:
* ?page=2 (Optional, specifies the page number of episodes to fetch and insert)
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 201,
* message: "Bulk insert episode operation completed successfully",
* data: { ...bulkInsertResult } // Data returned only if the env run on development mode
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
* See OpenAPI documentation for request/response schema.
*/
export const bulkInsertEpisodeController = async (
ctx: Context & { body: { media_mal_id: number }; query: { page?: number } },
) => {
export const bulkInsertEpisodeController = async (ctx: {
set: Context["set"];
body: Static<typeof bulkInsertEpisodeSchema.body>;
query: Static<typeof bulkInsertEpisodeSchema.query>;
}) => {
try {
const bulkInsertResult = await bulkInsertEpisodeService(
ctx.body.media_mal_id,
ctx.query.page,
);
return returnWriteResponse(
ctx.set,
201,
"Bulk insert episode operation completed successfully",
bulkInsertResult,
);
const bulkInsertResult = await bulkInsertEpisodeService(ctx.body.media_mal_id, ctx.query.page);
return returnWriteResponse(ctx.set, 201, "Bulk insert episode operation completed successfully", bulkInsertResult);
} catch (err) {
return mainErrorHandler(ctx.set, err);
}

View File

@ -1,47 +1,21 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { bulkInsertAnimeService } from "../services/http/bulkInsertAnime.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { bulkInsertMediaSchema } from "../schemas/bulkInsertMedia.schema";
/**
* @function bulkInsertMediaController
* @description Insert new anime to the database only with mal_id. This operation including inserting related data such as genres, studios, producers, licensors, themes, demographics, and relations.
* Insert anime and its related data into the database using MAL ID.
*
* @param {Context & { body: { mal_id: number } }} ctx
* The context object containing the request body.
* The body must include:
* - mal_id: number - The MyAnimeList ID of the anime to be inserted.
* This controller orchestrates the bulk insertion process including
* genres, studios, producers, licensors, themes, voice actors, and relations.
*
* @example
* Request route: POST /internal/anime/bulk-insert
* Request body:
* {
* "mal_id": 12345
* }
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 201,
* message: "Bulk insert anime operation completed successfully",
* data: { ...bulkInsertResult } // Data returned only if the env run on development mode
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
* See OpenAPI documentation for request/response schema.
*/
export const bulkInsertMediaController = async (
ctx: Context & { body: { mal_id: number } },
) => {
export const bulkInsertMediaController = async (ctx: {
set: Context["set"];
body: Static<typeof bulkInsertMediaSchema.body>;
}) => {
try {
const bulkInsertResult = await bulkInsertAnimeService(ctx.body.mal_id);
return returnWriteResponse(

View File

@ -1,87 +1,24 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { bulkInsertVideoService } from "../services/http/bulkInsertVideo.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
export interface BulkInsertVideoBodyRequest {
media_id: string;
data: Array<{
episode: number;
videos: Array<{
service_id: string;
video_code: string;
thumbnail_code?: string;
}>;
}>;
}
import { bulkInsertVideoSchema } from "../schemas/bulkInsertVideo.schema";
/**
* @function bulkInsertVideoController
* @description Perform bulk insert of videos for specific episodes of a media. This operation inserts multiple videos associated with different episodes into the database based on the provided data.
* Bulk insert videos into the database.
*
* @param {Context & { body: BulkInsertVideoBodyRequest }} ctx
* The context object containing the request body.
* The body must include:
* - media_id: string - The ID of the media for which episodes will be inserted.
* - data: Array - An array of episode data, each containing:
* - episode: number - The episode number.
* - videos: Array - An array of video data for the episode, each containing:
* - service_id: string - The ID of the video service.
* - code: string - The code of the video on the service.
* This controller handles the bulk insertion of videos by accepting an array of video data in the request body,
* invoking the service to perform the insertion, and returning a response with the inserted video details.
*
* @example
* Request route: POST /internal/video/bulk-insert
* Request body:
* {
* "media_id": "019c064e-a03d-7cc3-b2ae-5d6850ea456b",
* "data": [
* {
* "episode": 1,
* "videos": [
* {
* "service_id": "019c0df6-f8fe-7565-82cd-9c29b20232ab",
* "code": "fzwu9n8ge2qt"
* }
* ]
* },
* {
* "episode": 2,
* "videos": [
* {
* "service_id": "019c0df6-f8fe-7565-82cd-9c29b20232ab",
* "code": "w2maywh53rt8"
* }
* ]
* }
* ]
* },
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 201,
* message: "Videos inserted",
* data: { ...insertedVideos } // Data returned only if the env run on development mode
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
* See OpenAPI documentation for request/response schema.
*/
export const bulkInsertVideoController = async (
ctx: Context & { body: BulkInsertVideoBodyRequest },
) => {
export const bulkInsertVideoController = async (ctx: {
set: Context["set"];
body: Static<typeof bulkInsertVideoSchema.body>;
}) => {
try {
const insertedVideos = await bulkInsertVideoService(ctx.body);
return returnWriteResponse(ctx.set, 201, "Videos inserted", insertedVideos);
return returnWriteResponse(ctx.set, 201, "Videos inserted successfully", insertedVideos);
} catch (error) {
throw mainErrorHandler(ctx.set, error);
}

View File

@ -1,24 +1,25 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { createHeroBannerService } from "../services/http/createHeroBanner.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { createHeroBannerSchema } from "../schemas/createHeroBanner.schema";
export interface CreateHeroBannerRequestBody {
isClickable?: boolean;
title?: string;
tags: string[];
description?: string;
buttonContent?: string;
buttonLink?: string;
imageUrl?: string;
startDate: string;
endDate: string;
}
export const createHeroBannerController = async (
ctx: Context & { body: CreateHeroBannerRequestBody },
) => {
/**
* Create a new hero banner.
*
* This controller handles the creation of a hero banner by accepting the necessary
* data in the request body, invoking the service to create the banner, and returning
* an created payload response.
*
* See OpenAPI documentation for request/response schema.
*/
export const createHeroBannerController = async (ctx: {
set: Context["set"];
body: Static<typeof createHeroBannerSchema.body>;
}) => {
try {
return await createHeroBannerService(ctx.body);
const createdBanner = await createHeroBannerService(ctx.body);
return returnWriteResponse(ctx.set, 201, "Hero banner created successfully", createdBanner);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}

View File

@ -1,79 +1,26 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { createVideoServiceInternalService } from "../services/http/createVideoService.service";
export interface CreateVideoServiceInternalBodyRequest {
name: string;
domain: string;
logo: string;
hexColor: string;
endpointVideo: string;
endpointThumbnail: string;
endpointDownload?: string;
}
import { createVideoServiceInternalSchema } from "../schemas/createVideoServiceInternal.schema";
/**
* @function createVideoServiceInternalController
* @description Perform creation of a new video service. This operation adds a new video service to the database based on the provided data.
* Controller for creating a new video service.
*
* @param {Context & { body: CreateVideoServiceInternalBodyRequest }} ctx
* The context object containing the request body.
* The body must include:
* - name: string - The name of the video service.
* - domain: string - The domain of the video service.
* - logo: string - The logo URL of the video service.
* - hexColor: string - The hex color associated with the video service.
* - endpointVideo: string - The endpoint URL for video streaming.
* - endpointThumbnail: string - The endpoint URL for thumbnails.
* - endpointDownload?: string - (Optional) The endpoint URL for downloads.
* This controller handles the HTTP request for creating a new video service.
* It validates the incoming request body against the defined schema,
* invokes the service layer to perform the creation logic,
* and returns an appropriate HTTP response based on the outcome of the operation.
*
* @example
* Request route: POST /internal/video-service
* Request body:
* {
* "name": "Example Video Service",
* "domain": "example.com",
* "logo": "https://example.com/logo.png",
* "hexColor": "#FF5733",
* "endpointVideo": "https://api.example.com/videos",
* "endpointThumbnail": "https://api.example.com/thumbnails",
* "endpointDownload": "https://api.example.com/downloads"
* },
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 201,
* message: "Video service created",
* data: { ...createdVideoService } // Data returned only if the env run on development mode
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
* See OpenAPI documentation for request/response schema.
*/
export const createVideoServiceInternalController = async (
ctx: Context & { body: CreateVideoServiceInternalBodyRequest },
) => {
export const createVideoServiceInternalController = async (ctx: {
set: Context["set"];
body: Static<typeof createVideoServiceInternalSchema.body>;
}) => {
try {
const createdVideoService = await createVideoServiceInternalService(
ctx.body,
);
return returnWriteResponse(
ctx.set,
201,
"Video service created",
createdVideoService,
);
const createdVideoService = await createVideoServiceInternalService(ctx.body);
return returnWriteResponse(ctx.set, 201, "Video service created", createdVideoService);
} catch (error) {
throw mainErrorHandler(ctx.set, error);
}

View File

@ -3,15 +3,18 @@ import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { purgeUnusedSessionService } from "../services/http/purgeUnusedSession.service";
/**
* Controller for purging unused user sessions
*
* This controller handles the HTTP request for purging all unused user sessions. It will delete all unused sessions from the database based on their authentication status and deleted status.
* The response will indicate the success of the operation and may include details about the number of sessions purged if the environment is running in development mode.
*
* See OpenAPI documentation for request/response schema.
*/
export const purgeUnusedSessionController = async (ctx: Context) => {
try {
const result = await purgeUnusedSessionService();
return returnWriteResponse(
ctx.set,
200,
"Successfully purged all unused user sessions",
result,
);
return returnWriteResponse(ctx.set, 200, "Successfully purged all unused user sessions", result);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}

View File

@ -1,55 +1,24 @@
import { Context } from "elysia";
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { updateAllEpisodeThumbnailService } from "../services/http/updateAllEpisodeThumbnail.service";
import { updateAllEpisodeThumbnailSchema } from "../schemas/updateAllEpisodeThumbnail.schema";
/**
* @function updateAllEpisodeThumbnailController
* @description Controller to handle the bulk updating of episode thumbnails for all episodes associated with a specific service reference ID.
* Updating all episode thumbnails for a specific target service reference ID.
*
* @param {Context & { body: { service_reference_id: string } }} ctx
* The context object containing the request body.
* The body must include:
* - service_reference_id: string - The ID of the service to which the episodes belong.
* This controller handles the bulk update of episode thumbnails for all episodes associated with a specific service reference ID.
* It fetches the latest thumbnail data from external sources and updates the existing episode records in the database accordingly.
*
* @example
* Request route: PUT /internal/episode/update-thumbnails
* Request body:
* {
* "service_reference_id": "019c0df6-f8fe-7565-82cd-9c29b20232ab"
* },
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 204,
* message: "Updating {newEpisodeThumbnailsCount} episode thumbnails successfully.",
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
* See OpenAPI documentation for request/response schema.
*/
export const updateAllEpisodeThumbnailController = async (
ctx: Context & { body: { service_reference_id?: string } },
) => {
export const updateAllEpisodeThumbnailController = async (ctx: {
set: Context["set"];
body: Static<typeof updateAllEpisodeThumbnailSchema.body>;
}) => {
try {
const newEpisodeThumbnailsCount = await updateAllEpisodeThumbnailService(
ctx.body.service_reference_id,
);
return returnWriteResponse(
ctx.set,
204,
`Updating ${newEpisodeThumbnailsCount} episode thumbnails successfully.`,
);
const newEpisodeThumbnailsCount = await updateAllEpisodeThumbnailService(ctx.body.service_reference_id);
return returnWriteResponse(ctx.set, 204, `Updating ${newEpisodeThumbnailsCount} episode thumbnails successfully.`);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}

View File

@ -1,4 +1,4 @@
import Elysia, { t } from "elysia";
import Elysia from "elysia";
import { bulkInsertEpisodeController } from "./controllers/bulkInsertEpisode.controller";
import { bulkInsertMediaController } from "./controllers/bulkInsertMedia.controller";
import { createVideoServiceInternalController } from "./controllers/createVideoService.controller";
@ -7,15 +7,21 @@ import { updateAllEpisodeThumbnailController } from "./controllers/updateAllEpis
import { purgeUnusedSessionController } from "./controllers/purgeUnusedSession.controller";
import { createHeroBannerController } from "./controllers/createHeroBanner.controller";
import { bulkInsertMediaSchema } from "./schemas/bulkInsertMedia.schema";
import { bulkInsertEpisodeSchema } from "./schemas/bulkInsertEpisode.schema";
import { updateAllEpisodeThumbnailSchema } from "./schemas/updateAllEpisodeThumbnail.schema";
import { bulkInsertVideoSchema } from "./schemas/bulkInsertVideo.schema";
import { createVideoServiceInternalSchema } from "./schemas/createVideoServiceInternal.schema";
import { purgeUnusedSessionSchema } from "./schemas/purgeUnusedSession.schema";
import { createHeroBannerSchema } from "./schemas/createHeroBanner.schema";
export const internalModule = new Elysia({
prefix: "/internal",
tags: ["Internal"],
})
.post("/media/bulk-insert", bulkInsertMediaController, bulkInsertMediaSchema)
.post("/episode/bulk-insert", bulkInsertEpisodeController)
.put("/episode/update-thumbnails", updateAllEpisodeThumbnailController)
.post("/video/bulk-insert", bulkInsertVideoController)
.post("/video-service", createVideoServiceInternalController)
.post("/user-session/purge-unused", purgeUnusedSessionController)
.post("/hero-banner", createHeroBannerController);
.post("/episode/bulk-insert", bulkInsertEpisodeController, bulkInsertEpisodeSchema)
.put("/episode/update-thumbnails", updateAllEpisodeThumbnailController, updateAllEpisodeThumbnailSchema)
.post("/video/bulk-insert", bulkInsertVideoController, bulkInsertVideoSchema)
.post("/video-service", createVideoServiceInternalController, createVideoServiceInternalSchema)
.post("/user-session/purge-unused", purgeUnusedSessionController, purgeUnusedSessionSchema)
.post("/hero-banner", createHeroBannerController, createHeroBannerSchema);

View File

@ -1,12 +1,12 @@
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";
import { SystemAccountId } from "../../../config/account/system";
import { Static } from "elysia";
import { createHeroBannerSchema } from "../schemas/createHeroBanner.schema";
import { Prisma } from "@prisma/client";
export const insertHeroBannerRepository = async (
payload: Omit<Prisma.HeroBannerCreateInput, "id" | "createdBy">,
) => {
export const insertHeroBannerRepository = async (payload: Static<typeof createHeroBannerSchema.body>) => {
try {
return await prisma.heroBanner.create({
data: {
@ -16,6 +16,9 @@ export const insertHeroBannerRepository = async (
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
throw new AppError(400, "A hero banner with the order priority already exists", error);
}
throw new AppError(500, "Failed to insert hero banner", error);
}
};

View File

@ -0,0 +1,73 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const bulkInsertEpisodeSchema = {
body: t.Object({
media_mal_id: t.Number({
description: "The MyAnimeList ID of the media for which episodes will be inserted",
}),
}),
query: t.Object({
page: t.Optional(
t.Number({
description: "Episode page number to fetch",
}),
),
}),
detail: {
summary: "Bulk insert episodes for a media",
description:
"Perform bulk insert of episodes for a specific media. This operation fetches episode data from external sources and inserts them into the database. The page parameter is optional; if not provided, the first page of episodes will be fetched.",
responses: {
201: {
description:
"Bulk insert episode operation completed successfully (Data returned only if the env run on development mode)",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 201 },
message: {
type: "string",
default: "Bulk insert episode operation completed successfully",
},
data: {
type: "object",
properties: {
pagination: {
type: "object",
properties: {
last_visible_page: { type: "integer", default: 1 },
has_next_page: { type: "boolean", default: false },
},
},
data: {
type: "array",
items: {
type: "object",
properties: {
mal_id: { type: "integer", default: 1 },
url: { type: "string", default: "https://myanimelist.net/anime/1" },
title: { type: "string", default: "Example Episode Title" },
title_japanese: { type: "string", default: "例のエピソードタイトル" },
title_romanji: { type: "string", default: "Rei no Episōdo Taitoru" },
aired: { type: "string", format: "date-time", default: "2022-01-01T00:00:00.000Z" },
score: { type: "number", default: 8.5 },
filler: { type: "boolean", default: false },
recap: { type: "boolean", default: false },
forum_url: { type: "string", default: "https://myanimelist.net/forum/1" },
},
},
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -3,47 +3,94 @@ import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const bulkInsertMediaSchema = {
body: t.Object({
media_mal_id: t.Number({
mal_id: t.Number({
description:
"The MyAnimeList ID of the media for which episodes will be inserted",
}),
}),
query: t.Object({
page: t.Optional(
t.Number({
description: "Episode page number to fetch",
}),
),
}),
response: {
201: t.Object({
success: t.Boolean({ default: true }),
status: t.Number(),
message: t.String(),
data: t.Optional(
t.Unknown(),
),
}),
404: t.Object({
success: t.Boolean({ default: false }),
status: t.Number(),
message: t.String(),
error: t.Optional(
t.Unknown(),
),
}),
500: t.Object({
success: t.Optional(t.Boolean({ default: false })),
status: t.Number(),
message: t.String(),
error: t.Optional(
t.Unknown(),
),
}),
},
detail: {
summary: "Bulk insert media",
description:
"Fetch media data from external sources and insert them into database",
responses: {
201: {
description: "Bulk insert media operation completed successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 201 },
message: {
type: "string",
default: "Bulk insert anime operation completed successfully",
},
data: {
type: "object",
properties: {
status: { type: "string", default: "airing" },
id: {
type: "string",
default: "019cc6c9-80b2-7f9a-b1b4-c8fb612ed481",
},
title: { type: "string", default: "Sakamoto Days" },
titleAlternative: { type: "object", default: {} },
slug: { type: "string", default: "sakamoto-days" },
malId: { type: "integer", default: 58939 },
pictureMedium: {
type: "string",
default:
"https://myanimelist.net/images/anime/1026/146459.webp",
},
pictureLarge: {
type: "string",
default:
"https://myanimelist.net/images/anime/1026/146459.webp",
},
country: { type: "string", default: "JP" },
score: { type: "string", default: "9.0" },
startAiring: {
type: "string",
format: "date-time",
default: "2022-07-01T00:00:00.000Z",
},
endAiring: {
type: "string",
format: "date-time",
default: "2022-07-01T00:00:00.000Z",
},
synopsis: {
type: "string",
default: "No synopsis available",
},
ageRating: { type: "string", default: "PG-13" },
mediaType: { type: "string", default: "ANIME" },
source: { type: "string" },
onDraft: { type: "boolean", default: false },
uploadedBy: { type: "string", default: "system" },
deletedAt: {
type: "string",
format: "date-time",
default: "2022-07-01T00:00:00.000Z",
},
createdAt: {
type: "string",
format: "date-time",
default: "2022-07-01T00:00:00.000Z",
},
updatedAt: {
type: "string",
format: "date-time",
default: "2022-07-01T00:00:00.000Z",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,63 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const bulkInsertVideoSchema = {
body: t.Object({
media_id: t.String({
description: "The ID of the media for which episodes will be inserted",
}),
data: t.Array(
t.Object({
episode: t.Number({
description: "The episode number",
}),
videos: t.Array(
t.Object({
service_id: t.String({
description: "The ID of the video service",
}),
video_code: t.String({
description: "The code of the video on the service",
}),
thumbnail_code: t.Optional(
t.String({
description: "The code of the thumbnail for the video on the service",
}),
),
}),
),
}),
),
}),
detail: {
summary: "Bulk insert videos for a media episode",
description:
"Perform bulk insert of videos for specific episodes of a media. This operation inserts multiple videos associated with different episodes into the database based on the provided data.",
responses: {
201: {
description: "Videos inserted successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 201 },
message: { type: "string", default: "Videos inserted successfully" },
data: {
type: "array",
default: ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
description: "An array of IDs of the inserted videos",
items: {
type: "string",
description: "The ID of the inserted video",
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,79 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const createHeroBannerSchema = {
body: t.Object({
orderPriority: t.Optional(
t.Number({ description: "The priority order of the hero banner. Lower numbers indicate higher priority." }),
),
mediaId: t.String({ description: "The ID of the media associated with the hero banner" }),
imageUrl: t.Optional(
t.String({
description:
"The URL of the image used in the hero banner. If not provided, a thumbnail image of the media will be used.",
}),
),
startDate: t.Date({ description: "The start date for the hero banner in ISO 8601 format" }),
endDate: t.Date({ description: "The end date for the hero banner in ISO 8601 format" }),
}),
detail: {
summary: "Create a new hero banner",
description:
"Perform creation of a new hero banner. This operation adds a new hero banner to the database based on the provided data.",
responses: {
201: {
description: "Hero banner created successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 201 },
message: { type: "string", default: "Hero banner created successfully" },
data: {
type: "object",
description:
"The created hero banner object. This field is returned only if the environment is running in development mode.",
properties: {
id: { type: "string", description: "The ID of the created hero banner" },
orderPriority: {
type: "number",
description: "The priority order of the hero banner. Lower numbers indicate higher priority.",
},
mediaId: { type: "string", description: "The ID of the media associated with the hero banner" },
imageUrl: {
type: "string",
description:
"The URL of the image used in the hero banner. If not provided, a thumbnail image of the media will be used.",
},
startDate: {
type: "string",
format: "date-time",
description: "The start date for the hero banner in ISO 8601 format",
},
endDate: {
type: "string",
format: "date-time",
description: "The end date for the hero banner in ISO 8601 format",
},
createdAt: {
type: "string",
format: "date-time",
description: "The timestamp when the hero banner was created",
},
updatedAt: {
type: "string",
format: "date-time",
description: "The timestamp when the hero banner was last updated",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,92 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const createVideoServiceInternalSchema = {
body: t.Object({
name: t.String({
description: "The name of the video service",
}),
domain: t.String({
description: "The domain of the video service",
}),
logo: t.String({
description: "The logo URL of the video service",
}),
hexColor: t.String({
description: "The hex color associated with the video service",
}),
endpointVideo: t.String({
description: "The endpoint URL for video streaming",
}),
endpointThumbnail: t.String({
description: "The endpoint URL for thumbnails",
}),
endpointDownload: t.Optional(
t.String({
description: "The endpoint URL for downloads",
}),
),
}),
detail: {
summary: "Create a new video service",
description:
"Perform creation of a new video service. This operation adds a new video service to the database based on the provided data.",
responses: {
201: {
description: "Video service created successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 201 },
message: { type: "string", default: "Video service created" },
data: {
type: "object",
description:
"The created video service object. This field is returned only if the environment is running in development mode.",
properties: {
id: { type: "string", description: "The ID of the created video service" },
name: { type: "string", description: "The name of the video service" },
domain: { type: "string", description: "The domain of the video service" },
logo: { type: "string", description: "The logo URL of the video service" },
hexColor: { type: "string", description: "The hex color associated with the video service" },
endpointVideo: { type: "string", description: "The endpoint URL for video streaming" },
endpointThumbnail: { type: "string", description: "The endpoint URL for thumbnails" },
endpointDownload: {
type: "string",
description:
"The endpoint URL for downloads. This field is optional and may be null if not provided.",
},
createdAt: {
type: "string",
format: "date-time",
description: "The timestamp when the video service was created",
},
updatedAt: {
type: "string",
format: "date-time",
description: "The timestamp when the video service was last updated",
},
deletedAt: {
type: "string",
format: "date-time",
description:
"The timestamp when the video service was deleted. This field is null if the video service is not deleted.",
},
createdBy: {
type: "string",
description:
"The ID of the account that created the video service (filled with the system account ID)",
},
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,34 @@
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const purgeUnusedSessionSchema = {
detail: {
summary: "Purge all unused user sessions",
description:
"Perform purge of all unused user sessions. This operation deletes all user sessions from the database that are considered unused based on authentication status and deleted status. This helps in maintaining a clean session store and improving security by removing stale sessions.",
responses: {
200: {
description: "Unused user sessions purged successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 200 },
message: { type: "string", default: "Successfully purged all unused user sessions" },
data: {
type: "object",
description:
"An object containing details about the purge operation. This field is returned only if the environment is running in development mode.",
properties: {
count: { type: "integer", description: "The number of user sessions that were purged" },
},
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -0,0 +1,35 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const updateAllEpisodeThumbnailSchema = {
body: t.Object({
service_reference_id: t.String({
description: "The ID of the service to which the target of episode thumbnails belong",
}),
}),
detail: {
summary: "Bulk update episode thumbnails",
description:
"Perform bulk update of episode thumbnails for all episodes associated with a specific service reference ID. This operation fetches the latest thumbnail data from external sources and updates the existing episode records in the database accordingly.",
responses: {
204: {
description: "Updating episode thumbnails operation completed successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 204 },
message: {
type: "string",
default: "Updating {newEpisodeThumbnailsCount} episode thumbnails operation completed successfully",
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -1,5 +1,4 @@
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";
@ -9,6 +8,7 @@ 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/jikan/media.reference";
export const bulkInsertAnimeService = async (malId: number) => {
try {
@ -24,8 +24,8 @@ export const bulkInsertAnimeService = async (malId: number) => {
const constructMediaPayload: Prisma.MediaUpsertArgs["create"] = {
id: generateUUIDv7(),
title: mediaFullInfo.data.title,
titleAlternative: (mediaFullInfo.data
.titles as unknown) as Prisma.InputJsonValue,
titleAlternative: mediaFullInfo.data
.titles as unknown as Prisma.InputJsonValue,
slug: await generateSlug(mediaFullInfo.data.title, {
model: "media",
target: "slug",

View File

@ -1,10 +1,10 @@
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";
import { getEpisodeReferenceAPI } from "../../../../config/apis/jikan/episode.reference";
export const bulkInsertEpisodeService = async (
mal_id: number,

View File

@ -1,12 +1,11 @@
import { SystemAccountId } from "../../../../config/account/system";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { BulkInsertVideoBodyRequest } from "../../controllers/bulkInsertVideo.controller";
import { findEpisodeWithMediaIdRepository } from "../../repositories/findEpisodeWithMediaId.repository";
import { bulkInsertVideoRepository } from "../../repositories/bulkInsertVideo.repository";
import { Static } from "elysia";
import { bulkInsertVideoSchema } from "../../schemas/bulkInsertVideo.schema";
export const bulkInsertVideoService = async (
body: BulkInsertVideoBodyRequest,
) => {
export const bulkInsertVideoService = async (body: Static<typeof bulkInsertVideoSchema.body>) => {
try {
const insertedVideos: string[] = [];
for (const episodeData of body.data) {

View File

@ -1,10 +1,9 @@
import { Static } from "elysia";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { CreateHeroBannerRequestBody } from "../../controllers/createHeroBanner.controller";
import { insertHeroBannerRepository } from "../../repositories/insertHeroBanner.repository";
import { createHeroBannerSchema } from "../../schemas/createHeroBanner.schema";
export const createHeroBannerService = async (
payload: CreateHeroBannerRequestBody,
) => {
export const createHeroBannerService = async (payload: Static<typeof createHeroBannerSchema.body>) => {
try {
return await insertHeroBannerRepository(payload);
} catch (error) {

View File

@ -1,11 +1,10 @@
import { Static } from "elysia";
import { SystemAccountId } from "../../../../config/account/system";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { CreateVideoServiceInternalBodyRequest } from "../../controllers/createVideoService.controller";
import { createVideoServiceInternalRepository } from "../../repositories/createVideoService.repository";
import { createVideoServiceInternalSchema } from "../../schemas/createVideoServiceInternal.schema";
export const createVideoServiceInternalService = async (
body: CreateVideoServiceInternalBodyRequest,
) => {
export const createVideoServiceInternalService = async (body: Static<typeof createVideoServiceInternalSchema.body>) => {
try {
return await createVideoServiceInternalRepository({
name: body.name,

View File

@ -1,5 +1,5 @@
import { SystemAccountId } from "../../../../config/account/system";
import { getContentReferenceAPI } from "../../../../config/apis/media.reference";
import { getContentReferenceAPI } from "../../../../config/apis/jikan/media.reference";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { bulkInsertCharactersRepository } from "../../repositories/bulkInsertCharacters.repository";
import { bulkInsertLangVARepository } from "../../repositories/bulkInsertLangVA.repository";