🚧 wip: create bulk insert endpoint
This commit is contained in:
3
bun.lock
3
bun.lock
@ -24,6 +24,7 @@
|
|||||||
"mock-aws-s3": "^4.0.2",
|
"mock-aws-s3": "^4.0.2",
|
||||||
"nock": "^14.0.4",
|
"nock": "^14.0.4",
|
||||||
"pg": "^8.17.1",
|
"pg": "^8.17.1",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"zod": "^4.0.5",
|
"zod": "^4.0.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1150,6 +1151,8 @@
|
|||||||
|
|
||||||
"slice-ansi": ["slice-ansi@1.0.0", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0" } }, "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg=="],
|
"slice-ansi": ["slice-ansi@1.0.0", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0" } }, "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg=="],
|
||||||
|
|
||||||
|
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
|
||||||
|
|
||||||
"spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="],
|
"spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="],
|
||||||
|
|
||||||
"spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
|
"spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"lint": "bunx eslint",
|
"lint": "bunx eslint",
|
||||||
"commit": "bun x git-cz",
|
"commit": "bun x git-cz",
|
||||||
"push": "bun scripts/git-multipush.ts",
|
"push": "bun scripts/git-multipush.ts",
|
||||||
|
"prisma:push": "bunx prisma db push",
|
||||||
"prisma:generate": "bunx prisma generate",
|
"prisma:generate": "bunx prisma generate",
|
||||||
"prisma:dbml": "bunx prisma db pull && bunx prisma dbml --output ./prisma/dbml/schema.dbml",
|
"prisma:dbml": "bunx prisma db pull && bunx prisma dbml --output ./prisma/dbml/schema.dbml",
|
||||||
"prisma:seed": "bun run ./prisma/seed/index.ts",
|
"prisma:seed": "bun run ./prisma/seed/index.ts",
|
||||||
@ -35,6 +36,7 @@
|
|||||||
"mock-aws-s3": "^4.0.2",
|
"mock-aws-s3": "^4.0.2",
|
||||||
"nock": "^14.0.4",
|
"nock": "^14.0.4",
|
||||||
"pg": "^8.17.1",
|
"pg": "^8.17.1",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"zod": "^4.0.5"
|
"zod": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -6,23 +6,22 @@ Table medias {
|
|||||||
id String [pk]
|
id String [pk]
|
||||||
title String [not null]
|
title String [not null]
|
||||||
titleAlternative Json [not null]
|
titleAlternative Json [not null]
|
||||||
slug String [not null]
|
slug String [unique, not null]
|
||||||
|
malId Int [unique]
|
||||||
pictureMedium String [not null]
|
pictureMedium String [not null]
|
||||||
pictureLarge String [not null]
|
pictureLarge String [not null]
|
||||||
genres genres [not null]
|
genres genres [not null]
|
||||||
country countries [not null]
|
country Country [not null, default: 'JP']
|
||||||
countryId String [not null]
|
score Decimal [not null, default: 0]
|
||||||
isAiring Boolean [not null, default: false]
|
status String [not null]
|
||||||
isTba Boolean [not null, default: false]
|
startAiring DateTime
|
||||||
startAiring DateTime [not null]
|
endAiring DateTime
|
||||||
endAiring DateTime [not null]
|
|
||||||
synopsis String [not null]
|
synopsis String [not null]
|
||||||
nfsw Boolean [not null, default: false]
|
ageRating String [not null]
|
||||||
ageRating AgeRating [not null]
|
|
||||||
mediaType MediaType [not null]
|
mediaType MediaType [not null]
|
||||||
source Source [not null]
|
source String
|
||||||
studios studios [not null]
|
studios studios [not null]
|
||||||
pendingUpload Boolean [not null, default: true]
|
onDraft Boolean [not null, default: true]
|
||||||
uploader users [not null]
|
uploader users [not null]
|
||||||
uploadedBy String [not null]
|
uploadedBy String [not null]
|
||||||
deletedAt DateTime
|
deletedAt DateTime
|
||||||
@ -51,7 +50,7 @@ Table media_logs {
|
|||||||
Table genres {
|
Table genres {
|
||||||
id String [pk]
|
id String [pk]
|
||||||
name String [not null]
|
name String [not null]
|
||||||
slug String [not null]
|
slug String [unique, not null]
|
||||||
malId Int [not null]
|
malId Int [not null]
|
||||||
malUrl String [not null]
|
malUrl String [not null]
|
||||||
creator users [not null]
|
creator users [not null]
|
||||||
@ -66,9 +65,10 @@ Table genres {
|
|||||||
Table studios {
|
Table studios {
|
||||||
id String [pk]
|
id String [pk]
|
||||||
name String [not null]
|
name String [not null]
|
||||||
slug String [not null]
|
slug String [unique, not null]
|
||||||
logoUrl String [not null]
|
linkAbout String [not null]
|
||||||
colorHex String [not null]
|
malId Int [not null]
|
||||||
|
logoUrl String
|
||||||
creator users [not null]
|
creator users [not null]
|
||||||
createdBy String [not null]
|
createdBy String [not null]
|
||||||
deletedAt DateTime
|
deletedAt DateTime
|
||||||
@ -77,20 +77,6 @@ Table studios {
|
|||||||
medias medias [not null]
|
medias medias [not null]
|
||||||
}
|
}
|
||||||
|
|
||||||
Table countries {
|
|
||||||
id String [pk]
|
|
||||||
name String [not null]
|
|
||||||
code String [not null]
|
|
||||||
flag String [not null]
|
|
||||||
creator users [not null]
|
|
||||||
createdBy String [not null]
|
|
||||||
deletedAt DateTime
|
|
||||||
createdAt DateTime [default: `now()`, not null]
|
|
||||||
updatedAt DateTime [default: `now()`, not null]
|
|
||||||
medias medias [not null]
|
|
||||||
user_show_countries user_preferences [not null]
|
|
||||||
}
|
|
||||||
|
|
||||||
Table episodes {
|
Table episodes {
|
||||||
id String [pk]
|
id String [pk]
|
||||||
media medias [not null]
|
media medias [not null]
|
||||||
@ -186,7 +172,6 @@ Table users {
|
|||||||
media_approveds media_logs [not null]
|
media_approveds media_logs [not null]
|
||||||
genres genres [not null]
|
genres genres [not null]
|
||||||
studios studios [not null]
|
studios studios [not null]
|
||||||
countries countries [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]
|
||||||
@ -221,7 +206,7 @@ Table user_preferences {
|
|||||||
videoQuality VideoQuality [not null, default: 'Q1080']
|
videoQuality VideoQuality [not null, default: 'Q1080']
|
||||||
serviceDefault video_services
|
serviceDefault video_services
|
||||||
serviceDefaultId String
|
serviceDefaultId String
|
||||||
showContries countries [not null]
|
hideContries Country[] [not null]
|
||||||
favoriteGenres genres [not null]
|
favoriteGenres genres [not null]
|
||||||
createdAt DateTime [default: `now()`, not null]
|
createdAt DateTime [default: `now()`, not null]
|
||||||
updatedAt DateTime [default: `now()`, not null]
|
updatedAt DateTime [default: `now()`, not null]
|
||||||
@ -495,25 +480,11 @@ Table UserFavoriteGenres {
|
|||||||
favoritegenresId String [ref: > genres.id]
|
favoritegenresId String [ref: > genres.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
Table UserShowContries {
|
|
||||||
user_show_countriesId String [ref: > user_preferences.id]
|
|
||||||
showcontriesId String [ref: > countries.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
Table UserSelectedSharingCollention {
|
Table UserSelectedSharingCollention {
|
||||||
allowed_collectionsId String [ref: > collections.id]
|
allowed_collectionsId String [ref: > collections.id]
|
||||||
usersallowedId String [ref: > users.id]
|
usersallowedId String [ref: > users.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
Enum AgeRating {
|
|
||||||
G
|
|
||||||
PG
|
|
||||||
PG_13
|
|
||||||
R
|
|
||||||
R_plus
|
|
||||||
Rx
|
|
||||||
}
|
|
||||||
|
|
||||||
Enum MediaType {
|
Enum MediaType {
|
||||||
TV
|
TV
|
||||||
ONA
|
ONA
|
||||||
@ -523,11 +494,11 @@ Enum MediaType {
|
|||||||
Music
|
Music
|
||||||
}
|
}
|
||||||
|
|
||||||
Enum Source {
|
Enum Country {
|
||||||
original
|
JP
|
||||||
manga
|
EN
|
||||||
light_novel
|
ID
|
||||||
game
|
KR
|
||||||
}
|
}
|
||||||
|
|
||||||
Enum MediaOperation {
|
Enum MediaOperation {
|
||||||
@ -622,8 +593,6 @@ Enum TypeSystemNotification {
|
|||||||
toast
|
toast
|
||||||
}
|
}
|
||||||
|
|
||||||
Ref: medias.countryId > countries.id
|
|
||||||
|
|
||||||
Ref: medias.uploadedBy > users.id
|
Ref: medias.uploadedBy > users.id
|
||||||
|
|
||||||
Ref: media_logs.proposedBy > users.id
|
Ref: media_logs.proposedBy > users.id
|
||||||
@ -636,8 +605,6 @@ Ref: genres.createdBy > users.id
|
|||||||
|
|
||||||
Ref: studios.createdBy > users.id
|
Ref: studios.createdBy > users.id
|
||||||
|
|
||||||
Ref: countries.createdBy > users.id
|
|
||||||
|
|
||||||
Ref: episodes.mediaId > medias.id
|
Ref: episodes.mediaId > medias.id
|
||||||
|
|
||||||
Ref: episodes.uploadedBy > users.id
|
Ref: episodes.uploadedBy > users.id
|
||||||
|
|||||||
@ -32,16 +32,16 @@ model Media {
|
|||||||
pictureLarge String @db.Text
|
pictureLarge String @db.Text
|
||||||
genres Genre[] @relation("MediaGenres")
|
genres Genre[] @relation("MediaGenres")
|
||||||
country Country @default(JP)
|
country Country @default(JP)
|
||||||
status Status
|
score Decimal @db.Decimal(4, 2) @default(0.00)
|
||||||
|
status String
|
||||||
startAiring DateTime?
|
startAiring DateTime?
|
||||||
finishAiring DateTime?
|
endAiring DateTime?
|
||||||
synopsis String @db.Text
|
synopsis String @db.Text
|
||||||
nfsw Boolean @default(false)
|
ageRating String
|
||||||
ageRating AgeRating
|
|
||||||
mediaType MediaType
|
mediaType MediaType
|
||||||
source Source
|
source String?
|
||||||
studios Studio[] @relation("MediaStudios")
|
studios Studio[] @relation("MediaStudios")
|
||||||
onDraft Boolean @default(false)
|
onDraft Boolean @default(true)
|
||||||
uploader User @relation("UserUploadedMedias", fields: [uploadedBy], references: [id])
|
uploader User @relation("UserUploadedMedias", fields: [uploadedBy], references: [id])
|
||||||
uploadedBy String
|
uploadedBy String
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
@ -52,6 +52,11 @@ model Media {
|
|||||||
episodes Episode[] @relation("MediaEpisodes")
|
episodes Episode[] @relation("MediaEpisodes")
|
||||||
collections Collection[] @relation("MediaCollections")
|
collections Collection[] @relation("MediaCollections")
|
||||||
reviews MediaReview[] @relation("MediaReviews")
|
reviews MediaReview[] @relation("MediaReviews")
|
||||||
|
|
||||||
|
@@index([status, onDraft, deletedAt])
|
||||||
|
@@index([mediaType])
|
||||||
|
@@index([uploadedBy])
|
||||||
|
@@index([createdAt])
|
||||||
@@map("medias")
|
@@map("medias")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +78,7 @@ model MediaLog {
|
|||||||
model Genre {
|
model Genre {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
slug String @db.VarChar(255)
|
slug String @db.VarChar(255) @unique
|
||||||
malId Int
|
malId Int
|
||||||
malUrl String @db.VarChar(255)
|
malUrl String @db.VarChar(255)
|
||||||
creator User @relation("UserCreatedGenres", fields: [createdBy], references: [id])
|
creator User @relation("UserCreatedGenres", fields: [createdBy], references: [id])
|
||||||
@ -84,15 +89,17 @@ model Genre {
|
|||||||
|
|
||||||
medias Media[] @relation("MediaGenres")
|
medias Media[] @relation("MediaGenres")
|
||||||
user_favourite_genres UserPreference[] @relation("UserFavoriteGenres")
|
user_favourite_genres UserPreference[] @relation("UserFavoriteGenres")
|
||||||
|
|
||||||
@@map("genres")
|
@@map("genres")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Studio {
|
model Studio {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
slug String @db.VarChar(255)
|
slug String @db.VarChar(255) @unique
|
||||||
logoUrl String @db.Text
|
linkAbout String @db.Text
|
||||||
colorHex String @db.VarChar(10)
|
malId Int
|
||||||
|
logoUrl String? @db.Text
|
||||||
creator User @relation("UserCreatedStudios", fields: [createdBy], references: [id])
|
creator User @relation("UserCreatedStudios", fields: [createdBy], references: [id])
|
||||||
createdBy String
|
createdBy String
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
@ -103,6 +110,40 @@ model Studio {
|
|||||||
@@map("studios")
|
@@map("studios")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Character {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
name String
|
||||||
|
role CharacterRole
|
||||||
|
favorites Int @default(0)
|
||||||
|
imageUrl String?
|
||||||
|
smallImageUrl String?
|
||||||
|
createdBy User @relation("UserCreatedCharacters", fields: [creatorId], references: [id])
|
||||||
|
creatorId String
|
||||||
|
deletedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@@map("characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
model VoiceActor {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
malId Int
|
||||||
|
name String
|
||||||
|
birthday DateTime?
|
||||||
|
description String @db.Text
|
||||||
|
aboutUrl String
|
||||||
|
imageUrl String?
|
||||||
|
websiteUrl String?
|
||||||
|
createdBy User @relation("UserCreatedVoiceActors", fields: [creatorId], references: [id])
|
||||||
|
creatorId String
|
||||||
|
deletedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@@map("voice_actors")
|
||||||
|
}
|
||||||
|
|
||||||
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])
|
||||||
@ -205,6 +246,8 @@ model User {
|
|||||||
media_approveds MediaLog[] @relation("UserApprovedMedias")
|
media_approveds MediaLog[] @relation("UserApprovedMedias")
|
||||||
genres Genre[] @relation("UserCreatedGenres")
|
genres Genre[] @relation("UserCreatedGenres")
|
||||||
studios Studio[] @relation("UserCreatedStudios")
|
studios Studio[] @relation("UserCreatedStudios")
|
||||||
|
characters Character[] @relation("UserCreatedCharacters")
|
||||||
|
voice_actor VoiceActor[] @relation("UserCreatedVoiceActors")
|
||||||
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")
|
||||||
@ -523,14 +566,6 @@ model SystemLog {
|
|||||||
//// Prisma Enum Values ////
|
//// Prisma Enum Values ////
|
||||||
|
|
||||||
// Media Enum
|
// Media Enum
|
||||||
enum AgeRating {
|
|
||||||
G // All Ages
|
|
||||||
PG // Children
|
|
||||||
PG_13 // Teens 13 or older
|
|
||||||
R // 17+ (violance & profanity)
|
|
||||||
R_plus // Mild Nudity
|
|
||||||
Rx // Hentai
|
|
||||||
}
|
|
||||||
enum MediaType {
|
enum MediaType {
|
||||||
TV
|
TV
|
||||||
ONA
|
ONA
|
||||||
@ -539,17 +574,6 @@ enum MediaType {
|
|||||||
Special
|
Special
|
||||||
Music
|
Music
|
||||||
}
|
}
|
||||||
enum Source {
|
|
||||||
original
|
|
||||||
manga
|
|
||||||
light_novel
|
|
||||||
game
|
|
||||||
}
|
|
||||||
enum Status {
|
|
||||||
FINISHED_AIRING @map("Finished Airing")
|
|
||||||
CURRENTLY_AIRING @map("Currently Airing")
|
|
||||||
NOT_YET_AIRED @map("Not yet aired")
|
|
||||||
}
|
|
||||||
enum Country {
|
enum Country {
|
||||||
JP @map("Japanese")
|
JP @map("Japanese")
|
||||||
EN @map("English")
|
EN @map("English")
|
||||||
@ -557,6 +581,12 @@ enum Country {
|
|||||||
KR @map("Korea")
|
KR @map("Korea")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Character Enum
|
||||||
|
enum CharacterRole {
|
||||||
|
Main
|
||||||
|
Supporting
|
||||||
|
}
|
||||||
|
|
||||||
// MediaLog Enum
|
// MediaLog Enum
|
||||||
enum MediaOperation {
|
enum MediaOperation {
|
||||||
create
|
create
|
||||||
|
|||||||
15
snowflake.d.ts
vendored
Normal file
15
snowflake.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
declare module "node-snowflake" {
|
||||||
|
interface SnowflakeOptions {
|
||||||
|
workerId?: number;
|
||||||
|
datacenterId?: number;
|
||||||
|
sequence?: number;
|
||||||
|
epoch?: number | bigint | Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Snowflake {
|
||||||
|
constructor(options?: SnowflakeOptions);
|
||||||
|
generate(): bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Snowflake };
|
||||||
|
}
|
||||||
6
src/config/apis/media.reference.ts
Normal file
6
src/config/apis/media.reference.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const getMediaReferenceAPI = (malId: number) => {
|
||||||
|
return {
|
||||||
|
baseURL: "https://api.jikan.moe/v4",
|
||||||
|
getMediaFullInfo: `/anime/${malId}/full`,
|
||||||
|
};
|
||||||
|
};
|
||||||
50
src/helpers/characters/generateSlug.ts
Normal file
50
src/helpers/characters/generateSlug.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import slugify from "slugify";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { prisma } from "../../utils/databases/prisma/connection";
|
||||||
|
|
||||||
|
interface UniqueConfig {
|
||||||
|
model?: keyof PrismaClient;
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateSlug(
|
||||||
|
input: string,
|
||||||
|
config?: UniqueConfig,
|
||||||
|
): Promise<string> {
|
||||||
|
const baseSlug = slugify(input, { lower: true, strict: true });
|
||||||
|
let uniqueSlug = baseSlug;
|
||||||
|
|
||||||
|
// CASE 1: Tidak ada config → langsung return slug
|
||||||
|
if (!config) return uniqueSlug;
|
||||||
|
|
||||||
|
const { model, target } = config;
|
||||||
|
|
||||||
|
// CASE 2: Validasi pasangan model-target
|
||||||
|
if (!model || !target) {
|
||||||
|
throw new Error(`Both "model" and "target" must be provided together.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE 3: Cek unique
|
||||||
|
const prismaModel = (prisma as any)[model];
|
||||||
|
if (!prismaModel) {
|
||||||
|
throw new Error(`Model "${model as string}" not found in PrismaClient.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const exists = await prismaModel.findFirst({
|
||||||
|
where: {
|
||||||
|
[target]: uniqueSlug,
|
||||||
|
},
|
||||||
|
select: { [target]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!exists) break;
|
||||||
|
|
||||||
|
uniqueSlug = `${baseSlug}-${counter}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueSlug;
|
||||||
|
}
|
||||||
25
src/modules/internal/index.ts
Normal file
25
src/modules/internal/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
134
src/modules/internal/repositories/insertMedia.repository.ts
Normal file
134
src/modules/internal/repositories/insertMedia.repository.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
125
src/modules/internal/types/mediaFullInfo.type.ts
Normal file
125
src/modules/internal/types/mediaFullInfo.type.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { MediaType } from "@prisma/client";
|
||||||
|
export interface MediaFullInfoResponse {
|
||||||
|
data: Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
mal_id: number;
|
||||||
|
url: string;
|
||||||
|
images: { [key: string]: Image };
|
||||||
|
trailer: Trailer;
|
||||||
|
approved: boolean;
|
||||||
|
titles: Title[];
|
||||||
|
title: string;
|
||||||
|
title_english: string;
|
||||||
|
title_japanese: string;
|
||||||
|
title_synonyms: string[];
|
||||||
|
type: MediaType;
|
||||||
|
source: string;
|
||||||
|
episodes: number;
|
||||||
|
status: string;
|
||||||
|
airing: boolean;
|
||||||
|
aired: Aired;
|
||||||
|
duration: string;
|
||||||
|
rating: string;
|
||||||
|
score: number;
|
||||||
|
scored_by: number;
|
||||||
|
rank: number;
|
||||||
|
popularity: number;
|
||||||
|
members: number;
|
||||||
|
favorites: number;
|
||||||
|
synopsis: string;
|
||||||
|
background: string;
|
||||||
|
season: string;
|
||||||
|
year: number;
|
||||||
|
broadcast: Broadcast;
|
||||||
|
producers: Genre[];
|
||||||
|
licensors: any[];
|
||||||
|
studios: Genre[];
|
||||||
|
genres: Genre[];
|
||||||
|
explicit_genres: any[];
|
||||||
|
themes: Genre[];
|
||||||
|
demographics: any[];
|
||||||
|
relations: Relation[];
|
||||||
|
theme: Theme;
|
||||||
|
external: External[];
|
||||||
|
streaming: External[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Aired {
|
||||||
|
from: Date;
|
||||||
|
to: Date;
|
||||||
|
prop: Prop;
|
||||||
|
string: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Prop {
|
||||||
|
from: From;
|
||||||
|
to: From;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface From {
|
||||||
|
day: number;
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Broadcast {
|
||||||
|
day: string;
|
||||||
|
time: string;
|
||||||
|
timezone: string;
|
||||||
|
string: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface External {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Genre {
|
||||||
|
mal_id: number;
|
||||||
|
type: Type;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
Anime = "anime",
|
||||||
|
Manga = "manga",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
image_url: string;
|
||||||
|
small_image_url: string;
|
||||||
|
large_image_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Relation {
|
||||||
|
relation: string;
|
||||||
|
entry: Genre[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Theme {
|
||||||
|
openings: string[];
|
||||||
|
endings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Title {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Trailer {
|
||||||
|
youtube_id: null;
|
||||||
|
url: null;
|
||||||
|
embed_url: string;
|
||||||
|
images: Images;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Images {
|
||||||
|
image_url: null;
|
||||||
|
small_image_url: null;
|
||||||
|
medium_image_url: null;
|
||||||
|
large_image_url: null;
|
||||||
|
maximum_image_url: null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user