refactor #25
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"parser": "typescript"
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
18
src/config/documentation/openAPI.ts
Normal file
18
src/config/documentation/openAPI.ts
Normal 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.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
8
src/helpers/types/InferSchema.ts
Normal file
8
src/helpers/types/InferSchema.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Static } from "elysia";
|
||||||
|
|
||||||
|
export type InferSchema<S> = {
|
||||||
|
body: S extends { body: any } ? Static<S["body"]> : never;
|
||||||
|
query: S extends { query: any } ? Static<S["query"]> : never;
|
||||||
|
params: S extends { params: any } ? Static<S["params"]> : never;
|
||||||
|
headers: S extends { headers: any } ? Static<S["headers"]> : never;
|
||||||
|
};
|
||||||
15
src/index.ts
15
src/index.ts
@ -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(
|
||||||
|
|||||||
@ -1,47 +1,22 @@
|
|||||||
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>;
|
||||||
|
query: Static<typeof bulkInsertMediaSchema.query>;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
const bulkInsertResult = await bulkInsertAnimeService(ctx.body.mal_id);
|
const bulkInsertResult = await bulkInsertAnimeService(ctx.body.mal_id);
|
||||||
return returnWriteResponse(
|
return returnWriteResponse(
|
||||||
|
|||||||
@ -3,7 +3,7 @@ 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",
|
||||||
}),
|
}),
|
||||||
@ -15,35 +15,89 @@ export const bulkInsertMediaSchema = {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
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;
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user