feat: create bulk insertion for characters

This commit is contained in:
Rafi Arrafif
2026-01-25 10:57:35 +07:00
parent fe10412f1a
commit 11834924e9
12 changed files with 198 additions and 6 deletions

View File

@ -90,6 +90,7 @@ Table characters {
deletedAt DateTime deletedAt DateTime
createdAt DateTime [default: `now()`, not null] createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null]
voice_actors lang_va_char [not null]
} }
Table voice_actors { Table voice_actors {
@ -106,6 +107,24 @@ Table voice_actors {
deletedAt DateTime deletedAt DateTime
createdAt DateTime [default: `now()`, not null] createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null]
characters lang_va_char [not null]
}
Table lang_va_char {
id String [pk]
language String [not null]
voiceActor voice_actors [not null]
vaId String [not null]
character characters [not null]
charId String [not null]
createdBy users [not null]
creatorId String [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
indexes {
(language, vaId, charId) [unique]
}
} }
Table episodes { Table episodes {
@ -205,6 +224,7 @@ Table users {
studios studios [not null] studios studios [not null]
characters characters [not null] characters characters [not null]
voice_actor voice_actors [not null] voice_actor voice_actors [not null]
lang_va_char lang_va_char [not null]
episodes episodes [not null] episodes episodes [not null]
episode_likes episode_likes [not null] episode_likes episode_likes [not null]
videos videos [not null] videos videos [not null]
@ -647,6 +667,12 @@ Ref: characters.creatorId > users.id
Ref: voice_actors.creatorId > users.id Ref: voice_actors.creatorId > users.id
Ref: lang_va_char.vaId > voice_actors.id
Ref: lang_va_char.charId > characters.id
Ref: lang_va_char.creatorId > users.id
Ref: episodes.mediaId > medias.id Ref: episodes.mediaId > medias.id
Ref: episodes.uploadedBy > users.id Ref: episodes.uploadedBy > users.id

View File

@ -124,6 +124,7 @@ model Character {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
voice_actors LangVAChar[] @relation("CharVALanguage")
@@map("characters") @@map("characters")
} }
@ -142,9 +143,26 @@ model VoiceActor {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
characters LangVAChar[] @relation("VACharLanguage")
@@map("voice_actors") @@map("voice_actors")
} }
model LangVAChar {
id String @id @default(uuid()) @db.Uuid
language String
voiceActor VoiceActor @relation("VACharLanguage", fields: [vaId], references: [id])
vaId String @db.Uuid
character Character @relation("CharVALanguage", fields: [charId], references: [id])
charId String @db.Uuid
createdBy User @relation("UserCreatedLangVAChar", fields: [creatorId], references: [id])
creatorId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@unique([language, vaId, charId])
@@map("lang_va_char")
}
model Episode { model Episode {
id String @id @default(uuid()) id String @id @default(uuid())
media Media @relation("MediaEpisodes", fields: [mediaId], references: [id]) media Media @relation("MediaEpisodes", fields: [mediaId], references: [id])
@ -249,6 +267,7 @@ model User {
studios Studio[] @relation("UserCreatedStudios") studios Studio[] @relation("UserCreatedStudios")
characters Character[] @relation("UserCreatedCharacters") characters Character[] @relation("UserCreatedCharacters")
voice_actor VoiceActor[] @relation("UserCreatedVoiceActors") voice_actor VoiceActor[] @relation("UserCreatedVoiceActors")
lang_va_char LangVAChar[] @relation("UserCreatedLangVAChar")
episodes Episode[] @relation("UserEpisodes") episodes Episode[] @relation("UserEpisodes")
episode_likes EpisodeLike[] @relation("UserEpisodeLikes") episode_likes EpisodeLike[] @relation("UserEpisodeLikes")
videos Video[] @relation("UserVideos") videos Video[] @relation("UserVideos")

View File

@ -0,0 +1 @@
export const SystemAccountId = "b734b9bc-b4ea-408f-a80e-0a837ce884da";

View File

@ -0,0 +1 @@
export const baseURL = "https://api.jikan.moe/v4";

View File

@ -1,6 +1,8 @@
import { baseURL } from "./baseUrl";
export const getContentReferenceAPI = (malId: number) => { export const getContentReferenceAPI = (malId: number) => {
return { return {
baseURL: "https://api.jikan.moe/v4", baseURL,
getMediaFullInfo: `/anime/${malId}/full`, getMediaFullInfo: `/anime/${malId}/full`,
getMediaCharactersWithVA: `/anime/${malId}/characters`, getMediaCharactersWithVA: `/anime/${malId}/characters`,
}; };

View File

@ -0,0 +1,8 @@
import { baseURL } from "./baseUrl";
export const getPeopleAPI = (malId: number) => {
return {
baseURL,
getPeopleInfo: `/people/${malId}`,
};
};

View File

@ -0,0 +1,18 @@
import { Prisma } from "@prisma/client";
import { AppError } from "../../../helpers/error/instances/app";
import { prisma } from "../../../utils/databases/prisma/connection";
export const bulkInsertCharactersRepository = async (
payload: Prisma.CharacterUpsertArgs["create"],
) => {
try {
return await prisma.character.upsert({
where: { malId: payload.malId },
create: payload,
update: payload,
select: { id: true },
});
} catch (error) {
throw new AppError(500, "Failed to bulk insert characters", error);
}
};

View File

@ -0,0 +1,24 @@
import { Prisma } from "@prisma/client";
import { AppError } from "../../../helpers/error/instances/app";
import { prisma } from "../../../utils/databases/prisma/connection";
export const bulkInsertLangVARepository = async (
payload: Prisma.LangVACharUpsertArgs["create"],
) => {
try {
const insertedVA = await prisma.langVAChar.upsert({
where: {
language_vaId_charId: {
language: payload.language,
vaId: payload.vaId!,
charId: payload.charId!,
},
},
create: payload,
update: {},
});
return insertedVA.id;
} catch (error) {
throw new AppError(500, "Failed to bulk insert VAs", error);
}
};

View File

@ -2,18 +2,17 @@ import { Prisma } from "@prisma/client";
import { AppError } from "../../../helpers/error/instances/app"; import { AppError } from "../../../helpers/error/instances/app";
import { prisma } from "../../../utils/databases/prisma/connection"; import { prisma } from "../../../utils/databases/prisma/connection";
export const bulkInsertVARepository = async ( export const bulkInsertVoiceActorRepository = async (
payload: Prisma.VoiceActorUpsertArgs["create"], payload: Prisma.VoiceActorUpsertArgs["create"],
) => { ) => {
try { try {
const insertedVA = await prisma.voiceActor.upsert({ return await prisma.voiceActor.upsert({
where: { malId: payload.malId }, where: { malId: payload.malId },
create: payload, create: payload,
update: payload, update: payload,
select: { id: true }, select: { id: true },
}); });
return insertedVA.id;
} catch (error) { } catch (error) {
throw new AppError(500, "Failed to bulk insert VAs", error); throw new AppError(500, "Failed to bulk insert voice actor", error);
} }
}; };

View File

@ -1,6 +1,10 @@
import { SystemAccountId } from "../../../../config/account/system";
import { getContentReferenceAPI } from "../../../../config/apis/media.reference"; import { getContentReferenceAPI } from "../../../../config/apis/media.reference";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { bulkInsertCharactersRepository } from "../../repositories/bulkInsertCharacters.repository";
import { bulkInsertLangVARepository } from "../../repositories/bulkInsertLangVA.repository";
import { MediaCharWithVAInfo } from "../../types/mediaCharWithVAInfo"; import { MediaCharWithVAInfo } from "../../types/mediaCharWithVAInfo";
import { bulkInsertStaffOrPeopleService } from "./bulkInsertStaffOrPeople.service";
export const bulkInsertCharWithVAService = async (malId: number) => { export const bulkInsertCharWithVAService = async (malId: number) => {
try { try {
@ -9,7 +13,45 @@ export const bulkInsertCharWithVAService = async (malId: number) => {
`${baseURL}${getMediaCharactersWithVA}`, `${baseURL}${getMediaCharactersWithVA}`,
).then((res) => res.json())) as MediaCharWithVAInfo; ).then((res) => res.json())) as MediaCharWithVAInfo;
return; const insertedCharacters = [];
for (const charEntry of charactersWithVAData.data) {
// Insert character if not exists
const characterInsertedId = await bulkInsertCharactersRepository({
malId: charEntry.character.mal_id,
name: charEntry.character.name,
role: charEntry.role,
favorites: charEntry.favorites,
imageUrl: charEntry.character.images.webp.image_url,
smallImageUrl: charEntry.character.images.webp.small_image_url,
creatorId: SystemAccountId,
});
// Insert character voice actors if not exists
const insertedVAs: { staffId: string; lang: string }[] = [];
for (const VAEntries of charEntry.voice_actors) {
const insertedVAId = await bulkInsertStaffOrPeopleService(
VAEntries.person.mal_id,
);
insertedVAs.push({
staffId: insertedVAId.id,
lang: VAEntries.language,
});
}
// Link character with inserted VAs
for (const langVA of insertedVAs) {
await bulkInsertLangVARepository({
language: langVA.lang,
vaId: langVA.staffId,
charId: characterInsertedId.id,
creatorId: SystemAccountId,
});
}
insertedCharacters.push(characterInsertedId);
}
return insertedCharacters;
} catch (error) { } catch (error) {
ErrorForwarder(error); ErrorForwarder(error);
} }

View File

@ -0,0 +1,27 @@
import { SystemAccountId } from "../../../../config/account/system";
import { getPeopleAPI } from "../../../../config/apis/people.reference";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { bulkInsertVoiceActorRepository } from "../../repositories/bulkInsertVoiceActor.repository";
import { PeopleInfoResponse } from "../../types/peopleInfo";
export const bulkInsertStaffOrPeopleService = async (malId: number) => {
try {
const { baseURL, getPeopleInfo } = getPeopleAPI(malId);
const peopleData = (await fetch(baseURL + getPeopleInfo).then((res) =>
res.json(),
)) as PeopleInfoResponse;
return await bulkInsertVoiceActorRepository({
malId: peopleData.data.mal_id,
name: peopleData.data.name,
birthday: peopleData.data.birthday,
description: peopleData.data.about,
aboutUrl: peopleData.data.url,
imageUrl: peopleData.data.images.jpg.image_url,
websiteUrl: peopleData.data.website_url,
creatorId: SystemAccountId,
});
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -0,0 +1,25 @@
export interface PeopleInfoResponse {
data: Data;
}
interface Data {
mal_id: number;
url: string;
website_url: null;
images: Images;
name: string;
given_name: null;
family_name: null;
alternate_names: any[];
birthday: Date;
favorites: number;
about: string;
}
interface Images {
jpg: Jpg;
}
interface Jpg {
image_url: string;
}