Compare commits

..

7 Commits

Author SHA1 Message Date
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
30 changed files with 844 additions and 570 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", "aws-sdk": "^2.1692.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cookie": "^1.1.1", "cookie": "^1.1.1",
"elysia": "latest", "elysia": "^1.4.27",
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"joi": "^17.13.3", "joi": "^17.13.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -51,6 +51,7 @@
"cz-emoji": "^1.3.2-canary.2", "cz-emoji": "^1.3.2-canary.2",
"eslint": "^9.29.0", "eslint": "^9.29.0",
"globals": "^16.2.0", "globals": "^16.2.0",
"prettier": "^3.8.1",
"prisma": "^7.2.0", "prisma": "^7.2.0",
"prisma-dbml-generator": "^0.12.0", "prisma-dbml-generator": "^0.12.0",
"typescript-eslint": "^8.34.1" "typescript-eslint": "^8.34.1"

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

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

View File

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

View File

@ -1,62 +1,23 @@
import { Context } from "elysia"; import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler"; import { mainErrorHandler } from "../../../helpers/error/handler";
import { bulkInsertEpisodeService } from "../services/http/bulkInsertEpisode.service"; import { bulkInsertEpisodeService } from "../services/http/bulkInsertEpisode.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { bulkInsertEpisodeSchema } from "../schemas/bulkInsertEpisode.schema";
/** /**
* @function bulkInsertMediaController * Perform bulk insert of episodes for a specific 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. * 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 * See OpenAPI documentation for request/response schema.
* 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
* }
*/ */
export const bulkInsertEpisodeController = async ( export const bulkInsertEpisodeController = async (ctx: {
ctx: Context & { body: { media_mal_id: number }; query: { page?: number } }, set: Context["set"];
) => { body: Static<typeof bulkInsertEpisodeSchema.body>;
query: Static<typeof bulkInsertEpisodeSchema.query>;
}) => {
try { try {
const bulkInsertResult = await bulkInsertEpisodeService( const bulkInsertResult = await bulkInsertEpisodeService(ctx.body.media_mal_id, ctx.query.page);
ctx.body.media_mal_id, return returnWriteResponse(ctx.set, 201, "Bulk insert episode operation completed successfully", bulkInsertResult);
ctx.query.page,
);
return returnWriteResponse(
ctx.set,
201,
"Bulk insert episode operation completed successfully",
bulkInsertResult,
);
} catch (err) { } catch (err) {
return mainErrorHandler(ctx.set, 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 { mainErrorHandler } from "../../../helpers/error/handler";
import { bulkInsertAnimeService } from "../services/http/bulkInsertAnime.service"; import { bulkInsertAnimeService } from "../services/http/bulkInsertAnime.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { bulkInsertMediaSchema } from "../schemas/bulkInsertMedia.schema";
/** /**
* @function bulkInsertMediaController * Insert anime and its related data into the database using MAL ID.
* @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.
* *
* @param {Context & { body: { mal_id: number } }} ctx * This controller orchestrates the bulk insertion process including
* The context object containing the request body. * genres, studios, producers, licensors, themes, voice actors, and relations.
* The body must include:
* - mal_id: number - The MyAnimeList ID of the anime to be inserted.
* *
* @example * See OpenAPI documentation for request/response schema.
* 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
* }
*/ */
export const bulkInsertMediaController = async ( export const bulkInsertMediaController = async (ctx: {
ctx: Context & { body: { mal_id: number } }, set: Context["set"];
) => { body: Static<typeof bulkInsertMediaSchema.body>;
}) => {
try { try {
const bulkInsertResult = await bulkInsertAnimeService(ctx.body.mal_id); const bulkInsertResult = await bulkInsertAnimeService(ctx.body.mal_id);
return returnWriteResponse( return returnWriteResponse(

View File

@ -1,87 +1,24 @@
import { Context } from "elysia"; import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler"; import { mainErrorHandler } from "../../../helpers/error/handler";
import { bulkInsertVideoService } from "../services/http/bulkInsertVideo.service"; import { bulkInsertVideoService } from "../services/http/bulkInsertVideo.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { bulkInsertVideoSchema } from "../schemas/bulkInsertVideo.schema";
export interface BulkInsertVideoBodyRequest {
media_id: string;
data: Array<{
episode: number;
videos: Array<{
service_id: string;
video_code: string;
thumbnail_code?: string;
}>;
}>;
}
/** /**
* @function bulkInsertVideoController * Bulk insert videos into the database.
* @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.
* *
* @param {Context & { body: BulkInsertVideoBodyRequest }} ctx * This controller handles the bulk insertion of videos by accepting an array of video data in the request body,
* The context object containing the request body. * invoking the service to perform the insertion, and returning a response with the inserted video details.
* 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.
* *
* @example * See OpenAPI documentation for request/response schema.
* 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
* }
*/ */
export const bulkInsertVideoController = async ( export const bulkInsertVideoController = async (ctx: {
ctx: Context & { body: BulkInsertVideoBodyRequest }, set: Context["set"];
) => { body: Static<typeof bulkInsertVideoSchema.body>;
}) => {
try { try {
const insertedVideos = await bulkInsertVideoService(ctx.body); 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) { } catch (error) {
throw mainErrorHandler(ctx.set, 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 { mainErrorHandler } from "../../../helpers/error/handler";
import { createHeroBannerService } from "../services/http/createHeroBanner.service"; import { createHeroBannerService } from "../services/http/createHeroBanner.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { createHeroBannerSchema } from "../schemas/createHeroBanner.schema";
export interface CreateHeroBannerRequestBody { /**
isClickable?: boolean; * Create a new hero banner.
title?: string; *
tags: string[]; * This controller handles the creation of a hero banner by accepting the necessary
description?: string; * data in the request body, invoking the service to create the banner, and returning
buttonContent?: string; * an created payload response.
buttonLink?: string; *
imageUrl?: string; * See OpenAPI documentation for request/response schema.
startDate: string; */
endDate: string; export const createHeroBannerController = async (ctx: {
} set: Context["set"];
body: Static<typeof createHeroBannerSchema.body>;
export const createHeroBannerController = async ( }) => {
ctx: Context & { body: CreateHeroBannerRequestBody },
) => {
try { try {
return await createHeroBannerService(ctx.body); const createdBanner = await createHeroBannerService(ctx.body);
return returnWriteResponse(ctx.set, 201, "Hero banner created successfully", createdBanner);
} catch (error) { } catch (error) {
return mainErrorHandler(ctx.set, 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 { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { createVideoServiceInternalService } from "../services/http/createVideoService.service"; import { createVideoServiceInternalService } from "../services/http/createVideoService.service";
import { createVideoServiceInternalSchema } from "../schemas/createVideoServiceInternal.schema";
export interface CreateVideoServiceInternalBodyRequest {
name: string;
domain: string;
logo: string;
hexColor: string;
endpointVideo: string;
endpointThumbnail: string;
endpointDownload?: string;
}
/** /**
* @function createVideoServiceInternalController * Controller for creating 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.
* *
* @param {Context & { body: CreateVideoServiceInternalBodyRequest }} ctx * This controller handles the HTTP request for creating a new video service.
* The context object containing the request body. * It validates the incoming request body against the defined schema,
* The body must include: * invokes the service layer to perform the creation logic,
* - name: string - The name of the video service. * and returns an appropriate HTTP response based on the outcome of the operation.
* - 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.
* *
* @example * See OpenAPI documentation for request/response schema.
* 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
* }
*/ */
export const createVideoServiceInternalController = async ( export const createVideoServiceInternalController = async (ctx: {
ctx: Context & { body: CreateVideoServiceInternalBodyRequest }, set: Context["set"];
) => { body: Static<typeof createVideoServiceInternalSchema.body>;
}) => {
try { try {
const createdVideoService = await createVideoServiceInternalService( const createdVideoService = await createVideoServiceInternalService(ctx.body);
ctx.body, return returnWriteResponse(ctx.set, 201, "Video service created", createdVideoService);
);
return returnWriteResponse(
ctx.set,
201,
"Video service created",
createdVideoService,
);
} catch (error) { } catch (error) {
throw mainErrorHandler(ctx.set, 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 { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { purgeUnusedSessionService } from "../services/http/purgeUnusedSession.service"; 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) => { export const purgeUnusedSessionController = async (ctx: Context) => {
try { try {
const result = await purgeUnusedSessionService(); const result = await purgeUnusedSessionService();
return returnWriteResponse( return returnWriteResponse(ctx.set, 200, "Successfully purged all unused user sessions", result);
ctx.set,
200,
"Successfully purged all unused user sessions",
result,
);
} catch (error) { } catch (error) {
return mainErrorHandler(ctx.set, 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 { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { updateAllEpisodeThumbnailService } from "../services/http/updateAllEpisodeThumbnail.service"; import { updateAllEpisodeThumbnailService } from "../services/http/updateAllEpisodeThumbnail.service";
import { updateAllEpisodeThumbnailSchema } from "../schemas/updateAllEpisodeThumbnail.schema";
/** /**
* @function updateAllEpisodeThumbnailController * Updating all episode thumbnails for a specific target service reference ID.
* @description Controller to handle the bulk updating of episode thumbnails for all episodes associated with a specific service reference ID.
* *
* @param {Context & { body: { service_reference_id: string } }} ctx * This controller handles the bulk update of episode thumbnails for all episodes associated with a specific service reference ID.
* The context object containing the request body. * It fetches the latest thumbnail data from external sources and updates the existing episode records in the database accordingly.
* The body must include:
* - service_reference_id: string - The ID of the service to which the episodes belong.
* *
* @example * See OpenAPI documentation for request/response schema.
* 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
* }
*/ */
export const updateAllEpisodeThumbnailController = async ( export const updateAllEpisodeThumbnailController = async (ctx: {
ctx: Context & { body: { service_reference_id?: string } }, set: Context["set"];
) => { body: Static<typeof updateAllEpisodeThumbnailSchema.body>;
}) => {
try { try {
const newEpisodeThumbnailsCount = await updateAllEpisodeThumbnailService( const newEpisodeThumbnailsCount = await updateAllEpisodeThumbnailService(ctx.body.service_reference_id);
ctx.body.service_reference_id, return returnWriteResponse(ctx.set, 204, `Updating ${newEpisodeThumbnailsCount} episode thumbnails successfully.`);
);
return returnWriteResponse(
ctx.set,
204,
`Updating ${newEpisodeThumbnailsCount} episode thumbnails successfully.`,
);
} catch (error) { } catch (error) {
return mainErrorHandler(ctx.set, error); return mainErrorHandler(ctx.set, error);
} }

View File

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

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 = { export const bulkInsertMediaSchema = {
body: t.Object({ body: t.Object({
media_mal_id: t.Number({ mal_id: t.Number({
description: description:
"The MyAnimeList ID of the media for which episodes will be inserted", "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: { detail: {
summary: "Bulk insert media", summary: "Bulk insert media",
description: description:
"Fetch media data from external sources and insert them into database", "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; } 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,107 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const createHeroBannerSchema = {
body: t.Object({
isClickable: t.Optional(
t.Boolean({
description: "Indicates whether the hero banner is clickable",
}),
),
title: t.Optional(
t.String({
description: "The title of the hero banner",
}),
),
tags: t.Array(t.String(), {
description: "An array of tags associated with the hero banner",
}),
description: t.Optional(
t.String({
description: "A brief description of the hero banner",
}),
),
buttonContent: t.Optional(
t.String({
description: "The text content of the button on the hero banner",
}),
),
buttonLink: t.Optional(
t.String({
description: "The URL that the button on the hero banner links to",
}),
),
imageUrl: t.Optional(
t.String({
description: "The URL of the image used in the hero banner",
}),
),
startDate: t.String({
description: "The start date for the hero banner in ISO 8601 format",
}),
endDate: t.String({
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" },
isClickable: { type: "boolean", description: "Indicates whether the hero banner is clickable" },
title: { type: "string", description: "The title of the hero banner" },
tags: {
type: "array",
items: { type: "string" },
description: "An array of tags associated with the hero banner",
},
description: { type: "string", description: "A brief description of the hero banner" },
buttonContent: { type: "string", description: "The text content of the button on the hero banner" },
buttonLink: { type: "string", description: "The URL that the button on the hero banner links to" },
imageUrl: { type: "string", description: "The URL of the image used in the hero banner" },
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 { Prisma } from "@prisma/client";
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";
@ -9,6 +8,7 @@ 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";
import { getContentReferenceAPI } from "../../../../config/apis/jikan/media.reference";
export const bulkInsertAnimeService = async (malId: number) => { export const bulkInsertAnimeService = async (malId: number) => {
try { try {
@ -24,8 +24,8 @@ export const bulkInsertAnimeService = async (malId: number) => {
const constructMediaPayload: Prisma.MediaUpsertArgs["create"] = { const constructMediaPayload: Prisma.MediaUpsertArgs["create"] = {
id: generateUUIDv7(), id: generateUUIDv7(),
title: mediaFullInfo.data.title, title: mediaFullInfo.data.title,
titleAlternative: (mediaFullInfo.data titleAlternative: mediaFullInfo.data
.titles as unknown) as Prisma.InputJsonValue, .titles as unknown as Prisma.InputJsonValue,
slug: await generateSlug(mediaFullInfo.data.title, { slug: await generateSlug(mediaFullInfo.data.title, {
model: "media", model: "media",
target: "slug", target: "slug",

View File

@ -1,10 +1,10 @@
import { getEpisodeReferenceAPI } from "../../../../config/apis/episode.reference";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type"; import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type";
import { getMediaByMalIdRepository } from "../../../media/repositories/GET/getMediaByMalId.repository"; import { getMediaByMalIdRepository } from "../../../media/repositories/GET/getMediaByMalId.repository";
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { SystemAccountId } from "../../../../config/account/system"; import { SystemAccountId } from "../../../../config/account/system";
import { bulkInsertEpisodesRepository } from "../../repositories/bulkInsertEpisodes.repository"; import { bulkInsertEpisodesRepository } from "../../repositories/bulkInsertEpisodes.repository";
import { getEpisodeReferenceAPI } from "../../../../config/apis/jikan/episode.reference";
export const bulkInsertEpisodeService = async ( export const bulkInsertEpisodeService = async (
mal_id: number, mal_id: number,

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { SystemAccountId } from "../../../../config/account/system"; 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 { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { bulkInsertCharactersRepository } from "../../repositories/bulkInsertCharacters.repository"; import { bulkInsertCharactersRepository } from "../../repositories/bulkInsertCharacters.repository";
import { bulkInsertLangVARepository } from "../../repositories/bulkInsertLangVA.repository"; import { bulkInsertLangVARepository } from "../../repositories/bulkInsertLangVA.repository";