generated from nhcarrigan/template
483 lines
13 KiB
TypeScript
483 lines
13 KiB
TypeScript
import { prisma } from "../lib/prisma";
|
|
import type {
|
|
Suggestion,
|
|
SuggestionStatus,
|
|
SuggestionEntity,
|
|
CreateSuggestionDto,
|
|
AcceptWithEditsDto,
|
|
} from "@library/shared-types";
|
|
import {
|
|
GameStatus,
|
|
BookStatus,
|
|
MusicType,
|
|
MusicStatus,
|
|
ShowType,
|
|
ShowStatus,
|
|
MangaStatus,
|
|
} from "@library/shared-types";
|
|
import { GameService } from "./game.service";
|
|
import { BookService } from "./book.service";
|
|
import { MusicService } from "./music.service";
|
|
import { ArtService } from "./art.service";
|
|
import { ShowService } from "./show.service";
|
|
import { MangaService } from "./manga.service";
|
|
|
|
const gameService = new GameService();
|
|
const bookService = new BookService();
|
|
const musicService = new MusicService();
|
|
const artService = new ArtService();
|
|
const showService = new ShowService();
|
|
const mangaService = new MangaService();
|
|
|
|
interface SuggestionUser {
|
|
id: string;
|
|
username: string;
|
|
avatar: string | null;
|
|
inDiscord: boolean;
|
|
isVip: boolean;
|
|
isMod: boolean;
|
|
isStaff: boolean;
|
|
}
|
|
|
|
function mapUser(user: {
|
|
id: string;
|
|
username: string;
|
|
avatar: string | null;
|
|
inDiscord: boolean;
|
|
isVip: boolean;
|
|
isMod: boolean;
|
|
isStaff: boolean;
|
|
}): SuggestionUser {
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
avatar: user.avatar,
|
|
inDiscord: user.inDiscord,
|
|
isVip: user.isVip,
|
|
isMod: user.isMod,
|
|
isStaff: user.isStaff,
|
|
};
|
|
}
|
|
|
|
function mapSuggestion(suggestion: {
|
|
id: string;
|
|
userId: string;
|
|
user: {
|
|
id: string;
|
|
username: string;
|
|
avatar: string | null;
|
|
inDiscord: boolean;
|
|
isVip: boolean;
|
|
isMod: boolean;
|
|
isStaff: boolean;
|
|
};
|
|
entityType: string;
|
|
status: string;
|
|
declineReason: string | null;
|
|
title: string;
|
|
gameData: unknown;
|
|
bookData: unknown;
|
|
musicData: unknown;
|
|
artData: unknown;
|
|
showData: unknown;
|
|
mangaData: unknown;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}): Suggestion {
|
|
return {
|
|
id: suggestion.id,
|
|
userId: suggestion.userId,
|
|
user: mapUser(suggestion.user) as Suggestion["user"],
|
|
entityType: suggestion.entityType as SuggestionEntity,
|
|
status: suggestion.status as SuggestionStatus,
|
|
declineReason: suggestion.declineReason ?? undefined,
|
|
title: suggestion.title,
|
|
gameData: suggestion.gameData as Suggestion["gameData"],
|
|
bookData: suggestion.bookData as Suggestion["bookData"],
|
|
musicData: suggestion.musicData as Suggestion["musicData"],
|
|
artData: suggestion.artData as Suggestion["artData"],
|
|
showData: suggestion.showData as Suggestion["showData"],
|
|
mangaData: suggestion.mangaData as Suggestion["mangaData"],
|
|
createdAt: suggestion.createdAt,
|
|
updatedAt: suggestion.updatedAt,
|
|
};
|
|
}
|
|
|
|
function getMusicType(type?: string): MusicType {
|
|
switch (type) {
|
|
case "ALBUM":
|
|
return MusicType.album;
|
|
case "SINGLE":
|
|
return MusicType.single;
|
|
case "EP":
|
|
return MusicType.ep;
|
|
default:
|
|
return MusicType.album;
|
|
}
|
|
}
|
|
|
|
function getShowType(type?: string): ShowType {
|
|
switch (type) {
|
|
case "TV_SERIES":
|
|
return ShowType.tvSeries;
|
|
case "ANIME":
|
|
return ShowType.anime;
|
|
case "FILM":
|
|
return ShowType.film;
|
|
case "DOCUMENTARY":
|
|
return ShowType.documentary;
|
|
default:
|
|
return ShowType.tvSeries;
|
|
}
|
|
}
|
|
|
|
export const SuggestionService = {
|
|
async getAllSuggestions(filters?: {
|
|
status?: SuggestionStatus;
|
|
entityType?: SuggestionEntity;
|
|
}): Promise<Suggestion[]> {
|
|
const suggestions = await prisma.suggestion.findMany({
|
|
where: {
|
|
...(filters?.status && { status: filters.status }),
|
|
...(filters?.entityType && { entityType: filters.entityType }),
|
|
},
|
|
include: {
|
|
user: true,
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
});
|
|
|
|
return suggestions.map(mapSuggestion);
|
|
},
|
|
|
|
async getUserSuggestions(userId: string): Promise<Suggestion[]> {
|
|
const suggestions = await prisma.suggestion.findMany({
|
|
where: {
|
|
userId,
|
|
},
|
|
include: {
|
|
user: true,
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
});
|
|
|
|
return suggestions.map(mapSuggestion);
|
|
},
|
|
|
|
async getSuggestionById(id: string): Promise<Suggestion | null> {
|
|
const suggestion = await prisma.suggestion.findUnique({
|
|
where: { id },
|
|
include: {
|
|
user: true,
|
|
},
|
|
});
|
|
|
|
if (!suggestion) {
|
|
return null;
|
|
}
|
|
|
|
return mapSuggestion(suggestion);
|
|
},
|
|
|
|
async createSuggestion(
|
|
userId: string,
|
|
data: CreateSuggestionDto
|
|
): Promise<Suggestion> {
|
|
const pendingCount = await prisma.suggestion.count({
|
|
where: {
|
|
userId,
|
|
status: "UNREVIEWED",
|
|
},
|
|
});
|
|
|
|
if (pendingCount >= 5) {
|
|
throw new Error("You can only have 5 pending suggestions at a time");
|
|
}
|
|
|
|
const entityDataField = `${data.entityType.toLowerCase()}Data`;
|
|
|
|
const suggestionData: Record<string, unknown> = {
|
|
userId,
|
|
entityType: data.entityType,
|
|
title: data.title,
|
|
};
|
|
|
|
// Extract the data without entityType and title
|
|
const { entityType: _et, title: _t, ...entityData } = data;
|
|
suggestionData[entityDataField] = entityData;
|
|
|
|
const suggestion = await prisma.suggestion.create({
|
|
data: suggestionData as Parameters<typeof prisma.suggestion.create>[0]["data"],
|
|
include: {
|
|
user: true,
|
|
},
|
|
});
|
|
|
|
return mapSuggestion(suggestion);
|
|
},
|
|
|
|
async acceptSuggestion(id: string): Promise<Suggestion> {
|
|
const suggestion = await prisma.suggestion.findUnique({
|
|
where: { id },
|
|
include: { user: true },
|
|
});
|
|
|
|
if (!suggestion) {
|
|
throw new Error("Suggestion not found");
|
|
}
|
|
|
|
if (suggestion.status !== "UNREVIEWED") {
|
|
throw new Error("Suggestion has already been reviewed");
|
|
}
|
|
|
|
// Create the entity based on type with "Want to X" status
|
|
switch (suggestion.entityType) {
|
|
case "GAME": {
|
|
const gameData = suggestion.gameData as {
|
|
platform?: string;
|
|
notes?: string;
|
|
coverImage?: string;
|
|
} | null;
|
|
await gameService.createGame({
|
|
title: suggestion.title,
|
|
platform: gameData?.platform,
|
|
status: GameStatus.backlog,
|
|
notes: gameData?.notes,
|
|
coverImage: gameData?.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
case "BOOK": {
|
|
const bookData = suggestion.bookData as {
|
|
author: string;
|
|
isbn?: string;
|
|
notes?: string;
|
|
coverImage?: string;
|
|
} | null;
|
|
await bookService.createBook({
|
|
title: suggestion.title,
|
|
author: bookData?.author ?? "Unknown",
|
|
isbn: bookData?.isbn,
|
|
status: BookStatus.toRead,
|
|
notes: bookData?.notes,
|
|
coverImage: bookData?.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
case "MUSIC": {
|
|
const musicData = suggestion.musicData as {
|
|
artist: string;
|
|
type: string;
|
|
notes?: string;
|
|
coverArt?: string;
|
|
} | null;
|
|
await musicService.createMusic({
|
|
title: suggestion.title,
|
|
artist: musicData?.artist ?? "Unknown",
|
|
type: getMusicType(musicData?.type),
|
|
status: MusicStatus.wantToListen,
|
|
notes: musicData?.notes,
|
|
coverArt: musicData?.coverArt,
|
|
});
|
|
break;
|
|
}
|
|
case "ART": {
|
|
const artData = suggestion.artData as {
|
|
artist: string;
|
|
description?: string;
|
|
imageUrl: string;
|
|
} | null;
|
|
await artService.createArt({
|
|
title: suggestion.title,
|
|
artist: artData?.artist ?? "Unknown",
|
|
description: artData?.description,
|
|
imageUrl: artData?.imageUrl ?? "",
|
|
});
|
|
break;
|
|
}
|
|
case "SHOW": {
|
|
const showData = suggestion.showData as {
|
|
type: string;
|
|
notes?: string;
|
|
coverImage?: string;
|
|
} | null;
|
|
await showService.createShow({
|
|
title: suggestion.title,
|
|
type: getShowType(showData?.type),
|
|
status: ShowStatus.wantToWatch,
|
|
notes: showData?.notes,
|
|
coverImage: showData?.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
case "MANGA": {
|
|
const mangaData = suggestion.mangaData as {
|
|
author: string;
|
|
notes?: string;
|
|
coverImage?: string;
|
|
} | null;
|
|
await mangaService.createManga({
|
|
title: suggestion.title,
|
|
author: mangaData?.author ?? "Unknown",
|
|
status: MangaStatus.wantToRead,
|
|
notes: mangaData?.notes,
|
|
coverImage: mangaData?.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update suggestion status
|
|
const updatedSuggestion = await prisma.suggestion.update({
|
|
where: { id },
|
|
data: { status: "ACCEPTED" },
|
|
include: { user: true },
|
|
});
|
|
|
|
return mapSuggestion(updatedSuggestion);
|
|
},
|
|
|
|
async acceptSuggestionWithEdits(id: string, editedData: AcceptWithEditsDto): Promise<Suggestion> {
|
|
const suggestion = await prisma.suggestion.findUnique({
|
|
where: { id },
|
|
include: { user: true },
|
|
});
|
|
|
|
if (!suggestion) {
|
|
throw new Error("Suggestion not found");
|
|
}
|
|
|
|
if (suggestion.status !== "UNREVIEWED") {
|
|
throw new Error("Suggestion has already been reviewed");
|
|
}
|
|
|
|
// Create the entity based on type with edited data
|
|
switch (suggestion.entityType) {
|
|
case "GAME": {
|
|
await gameService.createGame({
|
|
title: editedData.title || suggestion.title,
|
|
platform: editedData.platform,
|
|
status: GameStatus.backlog,
|
|
notes: editedData.notes,
|
|
coverImage: editedData.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
case "BOOK": {
|
|
await bookService.createBook({
|
|
title: editedData.title || suggestion.title,
|
|
author: editedData.author || "Unknown",
|
|
isbn: editedData.isbn,
|
|
status: BookStatus.toRead,
|
|
notes: editedData.notes,
|
|
coverImage: editedData.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
case "MUSIC": {
|
|
await musicService.createMusic({
|
|
title: editedData.title || suggestion.title,
|
|
artist: editedData.artist || "Unknown",
|
|
type: getMusicType(editedData.type),
|
|
status: MusicStatus.wantToListen,
|
|
notes: editedData.notes,
|
|
coverArt: editedData.coverArt || editedData.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
case "ART": {
|
|
await artService.createArt({
|
|
title: editedData.title || suggestion.title,
|
|
artist: editedData.artist || "Unknown",
|
|
description: editedData.description,
|
|
imageUrl: editedData.imageUrl || "",
|
|
});
|
|
break;
|
|
}
|
|
case "SHOW": {
|
|
await showService.createShow({
|
|
title: editedData.title || suggestion.title,
|
|
type: getShowType(editedData.type),
|
|
status: ShowStatus.wantToWatch,
|
|
notes: editedData.notes,
|
|
coverImage: editedData.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
case "MANGA": {
|
|
await mangaService.createManga({
|
|
title: editedData.title || suggestion.title,
|
|
author: editedData.author || "Unknown",
|
|
status: MangaStatus.wantToRead,
|
|
notes: editedData.notes,
|
|
coverImage: editedData.coverImage,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update suggestion status
|
|
const updatedSuggestion = await prisma.suggestion.update({
|
|
where: { id },
|
|
data: { status: "ACCEPTED" },
|
|
include: { user: true },
|
|
});
|
|
|
|
return mapSuggestion(updatedSuggestion);
|
|
},
|
|
|
|
async declineSuggestion(id: string, reason?: string): Promise<Suggestion> {
|
|
const suggestion = await prisma.suggestion.findUnique({
|
|
where: { id },
|
|
});
|
|
|
|
if (!suggestion) {
|
|
throw new Error("Suggestion not found");
|
|
}
|
|
|
|
if (suggestion.status !== "UNREVIEWED") {
|
|
throw new Error("Suggestion has already been reviewed");
|
|
}
|
|
|
|
const updatedSuggestion = await prisma.suggestion.update({
|
|
where: { id },
|
|
data: {
|
|
status: "DECLINED",
|
|
declineReason: reason,
|
|
},
|
|
include: { user: true },
|
|
});
|
|
|
|
return mapSuggestion(updatedSuggestion);
|
|
},
|
|
|
|
async deleteSuggestion(id: string, userId: string, isAdmin: boolean): Promise<Suggestion> {
|
|
const suggestion = await prisma.suggestion.findUnique({
|
|
where: { id },
|
|
include: { user: true },
|
|
});
|
|
|
|
if (!suggestion) {
|
|
throw new Error("Suggestion not found");
|
|
}
|
|
|
|
if (!isAdmin && suggestion.userId !== userId) {
|
|
throw new Error("You can only delete your own suggestions");
|
|
}
|
|
|
|
if (suggestion.status !== "UNREVIEWED") {
|
|
throw new Error("Cannot delete a suggestion that has already been reviewed");
|
|
}
|
|
|
|
await prisma.suggestion.delete({
|
|
where: { id },
|
|
});
|
|
|
|
return mapSuggestion(suggestion);
|
|
},
|
|
};
|