diff --git a/src/config/apis/media.reference.ts b/src/config/apis/media.reference.ts index b845a5e..9f6cae9 100644 --- a/src/config/apis/media.reference.ts +++ b/src/config/apis/media.reference.ts @@ -1,4 +1,4 @@ -export const getMediaReferenceAPI = (malId: number) => { +export const getContentReferenceAPI = (malId: number) => { return { baseURL: "https://api.jikan.moe/v4", getMediaFullInfo: `/anime/${malId}/full`, diff --git a/src/modules/internal/controllers/bulkInsertAnime.controller.ts b/src/modules/internal/controllers/bulkInsertAnime.controller.ts new file mode 100644 index 0000000..9bb78c3 --- /dev/null +++ b/src/modules/internal/controllers/bulkInsertAnime.controller.ts @@ -0,0 +1,20 @@ +import { Context } from "elysia"; +import { mainErrorHandler } from "../../../helpers/error/handler"; +import { bulkInsertAnimeService } from "../services/bulkInsertAnime.service"; +import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; + +export const bulkInsertAnimeController = async ( + ctx: Context & { body: { mal_id: number } }, +) => { + try { + const bulkInsertResult = await bulkInsertAnimeService(ctx.body.mal_id); + return returnWriteResponse( + ctx.set, + 201, + "Bulk insert anime operation completed successfully", + bulkInsertResult, + ); + } catch (error) { + return mainErrorHandler(ctx.set, error); + } +}; diff --git a/src/modules/internal/index.ts b/src/modules/internal/index.ts index 202f334..2406c4b 100644 --- a/src/modules/internal/index.ts +++ b/src/modules/internal/index.ts @@ -1,25 +1,7 @@ -import Elysia, { Context } from "elysia"; -import { MediaFullInfoResponse } from "./types/mediaFullInfo.type"; -import { InsertMediaRepository } from "./repositories/insertMedia.repository"; -import { mainErrorHandler } from "../../helpers/error/handler"; - -const masterSourceAPI = "https://api.jikan.moe/v4"; +import Elysia from "elysia"; +import { bulkInsertAnimeController } from "./controllers/bulkInsertAnime.controller"; export const internalModule = new Elysia({ prefix: "/internal" }).post( - "/medias", - async (ctx: Context & { body: { mal_id: number } }) => { - try { - const fullMediaData = await fetch( - `${masterSourceAPI}/anime/${ctx.body.mal_id}/full`, - ) - .then((res) => res.json()) - .then((data) => data as MediaFullInfoResponse); - - // return fullMediaData; - const createMedia = await InsertMediaRepository(fullMediaData); - return createMedia; - } catch (error) { - return mainErrorHandler(ctx.set, error); - } - }, + "/media/bulk-insert", + bulkInsertAnimeController, ); diff --git a/src/modules/internal/repositories/bulkInsertGenres.repository.ts b/src/modules/internal/repositories/bulkInsertGenres.repository.ts new file mode 100644 index 0000000..171ce48 --- /dev/null +++ b/src/modules/internal/repositories/bulkInsertGenres.repository.ts @@ -0,0 +1,44 @@ +import { generateSlug } from "../../../helpers/characters/generateSlug"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; +import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; + +/** + * Genres Insertion + * + * This section handles the insertion of genres associated with the media. + * It iterates over each genre in the media data, generates a slug for it, + * and performs an upsert operation to ensure that the genre is either created + * or updated in the database. The IDs of the inserted or updated genres are + * collected for later association with the media. + * + * @param data - The full media data containing genres information. + * @returns An array of IDs of the inserted or updated genres. + */ +export const bulkInsertGenresRepository = async ( + data: MediaFullInfoResponse, +) => { + try { + const genreIds: string[] = []; + for (const genre of data.data.genres) { + const slug = (await generateSlug(genre.name)) as string; + const genrePayload = { + name: genre.name, + malId: genre.mal_id, + malUrl: genre.url, + createdBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", + slug, + }; + const insertedGenre = await prisma.genre.upsert({ + where: { slug }, + create: genrePayload, + update: genrePayload, + select: { id: true }, + }); + genreIds.push(insertedGenre.id); + } + return genreIds; + } catch (error) { + throw new AppError(500, "Failed to insert genres", error); + } +}; diff --git a/src/modules/internal/repositories/bulkInsertStudios.repository.ts b/src/modules/internal/repositories/bulkInsertStudios.repository.ts new file mode 100644 index 0000000..26a10bf --- /dev/null +++ b/src/modules/internal/repositories/bulkInsertStudios.repository.ts @@ -0,0 +1,61 @@ +import { generateSlug } from "../../../helpers/characters/generateSlug"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; +import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; + +/** + * Studios Insertion + * + * This section manages the insertion of studios associated with the media. + * It processes each studio listed in the media data, generating a slug for + * each and performing an upsert operation to either create or update the + * studio record in the database. The IDs of the inserted or updated studios + * are collected for later association with the media. + * + * @param data - The full media data containing studios information. + * @returns An array of IDs of the inserted or updated studios. + */ +export const bulkInsertStudiosRepository = async ( + data: MediaFullInfoResponse, +) => { + try { + const studioIds: string[] = []; + for (const studio of data.data.studios) { + const slug = (await generateSlug(studio.name)) as string; + const studioPayload = { + name: studio.name, + malId: studio.mal_id, + linkAbout: studio.url, + createdBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", + slug, + }; + const insertedStudio = await prisma.studio.upsert({ + where: { slug }, + create: studioPayload, + update: studioPayload, + select: { id: true }, + }); + studioIds.push(insertedStudio.id); + } + for (const studio of data.data.producers) { + const slug = (await generateSlug(studio.name)) as string; + const studioPayload = { + name: studio.name, + malId: studio.mal_id, + linkAbout: studio.url, + createdBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", + slug, + }; + const insertedStudio = await prisma.studio.upsert({ + where: { slug }, + create: studioPayload, + update: studioPayload, + select: { id: true }, + }); + studioIds.push(insertedStudio.id); + } + return studioIds; + } catch (error) { + throw new AppError(500, "Failed to insert studios", error); + } +}; diff --git a/src/modules/internal/repositories/bulkinsertMediaa.repository.ts b/src/modules/internal/repositories/bulkinsertMediaa.repository.ts new file mode 100644 index 0000000..5e811c6 --- /dev/null +++ b/src/modules/internal/repositories/bulkinsertMediaa.repository.ts @@ -0,0 +1,36 @@ +import { Prisma } from "@prisma/client"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; +import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; + +/** + * Media Payload Construction and Upsert + * + * This section constructs the payload for the media insertion or update. + * It gathers all necessary information from the media data, including + * title, alternative titles, slug, associated genres and studios, score, + * images, status, airing dates, synopsis, age rating, media type, source, + * and other relevant details. This payload is then used in an upsert + * operation to ensure that the media record is either created or updated + * in the database. + * + * @param data - The full media data for constructing the media payload. + * @returns The inserted or updated media record. + */ +export const InsertMediaRepository = async ({ + malId, + payload, +}: { + malId: number; + payload: Prisma.MediaUpsertArgs["create"]; +}) => { + try { + return await prisma.media.upsert({ + where: { malId }, + update: payload, + create: payload, + }); + } catch (error) { + throw new AppError(500, "Failed to insert media", error); + } +}; diff --git a/src/modules/internal/repositories/insertMedia.repository.ts b/src/modules/internal/repositories/insertMedia.repository.ts deleted file mode 100644 index f3627b5..0000000 --- a/src/modules/internal/repositories/insertMedia.repository.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Prisma } from "@prisma/client"; -import { generateSlug } from "../../../helpers/characters/generateSlug"; -import { AppError } from "../../../helpers/error/instances/app"; -import { prisma } from "../../../utils/databases/prisma/connection"; -import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; - -export const InsertMediaRepository = async (data: MediaFullInfoResponse) => { - try { - /** - * Genres Insertion - * - * This section handles the insertion of genres associated with the media. - * It iterates over each genre in the media data, generates a slug for it, - * and performs an upsert operation to ensure that the genre is either created - * or updated in the database. The IDs of the inserted or updated genres are - * collected for later association with the media. - * - * @param data - The full media data containing genres information. - */ - const genreIds: string[] = []; - for (const genre of data.data.genres) { - const slug = (await generateSlug(genre.name)) as string; - const genrePayload = { - name: genre.name, - malId: genre.mal_id, - malUrl: genre.url, - createdBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", - slug, - }; - const insertedGenre = await prisma.genre.upsert({ - where: { slug }, - create: genrePayload, - update: genrePayload, - select: { id: true }, - }); - genreIds.push(insertedGenre.id); - } - - /** - * Studios Insertion - * - * This section manages the insertion of studios associated with the media. - * It processes each studio listed in the media data, generating a slug for - * each and performing an upsert operation to either create or update the - * studio record in the database. The IDs of the inserted or updated studios - * are collected for later association with the media. - * - * @param data - The full media data containing studios information. - */ - const studioIds: string[] = []; - for (const studio of data.data.studios) { - const slug = (await generateSlug(studio.name)) as string; - const studioPayload = { - name: studio.name, - malId: studio.mal_id, - linkAbout: studio.url, - createdBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", - slug, - }; - const insertedStudio = await prisma.studio.upsert({ - where: { slug }, - create: studioPayload, - update: studioPayload, - select: { id: true }, - }); - studioIds.push(insertedStudio.id); - } - for (const studio of data.data.producers) { - const slug = (await generateSlug(studio.name)) as string; - const studioPayload = { - name: studio.name, - malId: studio.mal_id, - linkAbout: studio.url, - createdBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", - slug, - }; - const insertedStudio = await prisma.studio.upsert({ - where: { slug }, - create: studioPayload, - update: studioPayload, - select: { id: true }, - }); - studioIds.push(insertedStudio.id); - } - - /** - * Media Payload Construction and Upsert - * - * This section constructs the payload for the media insertion or update. - * It gathers all necessary information from the media data, including - * title, alternative titles, slug, associated genres and studios, score, - * images, status, airing dates, synopsis, age rating, media type, source, - * and other relevant details. This payload is then used in an upsert - * operation to ensure that the media record is either created or updated - * in the database. - * - * @param data - The full media data for constructing the media payload. - */ - const construct = { - title: data.data.title, - titleAlternative: (data.data.titles as unknown) as Prisma.InputJsonValue, - slug: await generateSlug(data.data.title, { - model: "media", - target: "slug", - }), - malId: data.data.mal_id, - genres: { - connect: genreIds.map((id) => ({ id })), - }, - studios: { - connect: studioIds.map((id) => ({ id })), - }, - score: data.data.score, - pictureMedium: data.data.images.webp.image_url, - pictureLarge: data.data.images.webp.large_image_url, - status: data.data.status, - startAiring: data.data.aired.from, - endAiring: data.data.aired.to, - synopsis: data.data.synopsis, - ageRating: data.data.rating, - mediaType: data.data.type, - source: data.data.source, - onDraft: false, - uploadedBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", - }; - return await prisma.media.upsert({ - where: { malId: data.data.mal_id }, - update: construct, - create: construct, - }); - } catch (error) { - throw new AppError(500, "Failed to insert media", error); - } -}; diff --git a/src/modules/internal/services/bulkInsertAnime.service.ts b/src/modules/internal/services/bulkInsertAnime.service.ts new file mode 100644 index 0000000..57b3684 --- /dev/null +++ b/src/modules/internal/services/bulkInsertAnime.service.ts @@ -0,0 +1,57 @@ +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/bulkinsertMediaa.repository"; +import { bulkInsertStudiosRepository } from "../repositories/bulkInsertStudios.repository"; +import { MediaFullInfoResponse } from "../types/mediaFullInfo.type"; +import { generateSlug } from "../../../helpers/characters/generateSlug"; + +export const bulkInsertAnimeService = async (malId: number) => { + try { + const { baseURL, getMediaFullInfo } = getContentReferenceAPI(malId); + const mediaFullInfo = (await fetch(baseURL + getMediaFullInfo).then((res) => + res.json(), + )) as MediaFullInfoResponse; + + const insertedGenres = await bulkInsertGenresRepository(mediaFullInfo); + const insertedStudios = await bulkInsertStudiosRepository(mediaFullInfo); + + const constructMediaPayload = { + title: mediaFullInfo.data.title, + titleAlternative: (mediaFullInfo.data + .titles as unknown) as Prisma.InputJsonValue, + slug: await generateSlug(mediaFullInfo.data.title, { + model: "media", + target: "slug", + }), + malId: mediaFullInfo.data.mal_id, + genres: { + connect: insertedGenres.map((id) => ({ id })), + }, + studios: { + connect: insertedStudios.map((id) => ({ id })), + }, + score: mediaFullInfo.data.score, + pictureMedium: mediaFullInfo.data.images.webp.image_url, + pictureLarge: mediaFullInfo.data.images.webp.large_image_url, + status: mediaFullInfo.data.status, + startAiring: mediaFullInfo.data.aired.from, + endAiring: mediaFullInfo.data.aired.to, + synopsis: mediaFullInfo.data.synopsis, + ageRating: mediaFullInfo.data.rating, + mediaType: mediaFullInfo.data.type, + source: mediaFullInfo.data.source, + onDraft: false, + uploadedBy: "b734b9bc-b4ea-408f-a80e-0a837ce884da", + }; + const insertedMedia = await InsertMediaRepository({ + malId: mediaFullInfo.data.mal_id, + payload: constructMediaPayload, + }); + + return insertedMedia; + } catch (error) { + ErrorForwarder(error); + } +};