Files
reno-realworld-5/src/app/routes/article/article.service.ts
Scaffolder 8053e114cf initial commit
Change-Id: Ifa2442bb2e786af351ef5f575ca69828870c8b6e
2026-08-03 21:12:35 +00:00

653 lines
12 KiB
TypeScript

import slugify from 'slugify';
import prisma from '../../../prisma/prisma-client';
import HttpException from '../../models/http-exception.model';
import profileMapper from '../profile/profile.utils';
import articleMapper from './article.mapper';
import { Tag } from '../tag/tag.model';
const buildFindAllQuery = (query: any, id: number | undefined) => {
const queries: any = [];
const orAuthorQuery = [];
const andAuthorQuery = [];
orAuthorQuery.push({
demo: {
equals: true,
},
});
if (id) {
orAuthorQuery.push({
id: {
equals: id,
},
});
}
if ('author' in query) {
andAuthorQuery.push({
username: {
equals: query.author,
},
});
}
const authorQuery = {
author: {
OR: orAuthorQuery,
AND: andAuthorQuery,
},
};
queries.push(authorQuery);
if ('tag' in query) {
queries.push({
tagList: {
some: {
name: query.tag,
},
},
});
}
if ('favorited' in query) {
queries.push({
favoritedBy: {
some: {
username: {
equals: query.favorited,
},
},
},
});
}
return queries;
};
export const getArticles = async (query: any, id?: number) => {
const andQueries = buildFindAllQuery(query, id);
const articlesCount = await prisma.article.count({
where: {
AND: andQueries,
},
});
const articles = await prisma.article.findMany({
where: { AND: andQueries },
orderBy: {
createdAt: 'desc',
},
skip: Number(query.offset) || 0,
take: Number(query.limit) || 10,
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});
return {
articles: articles.map((article: any) => articleMapper(article, id)),
articlesCount,
};
};
export const getFeed = async (offset: number, limit: number, id: number) => {
const articlesCount = await prisma.article.count({
where: {
author: {
followedBy: { some: { id: id } },
},
},
});
const articles = await prisma.article.findMany({
where: {
author: {
followedBy: { some: { id: id } },
},
},
orderBy: {
createdAt: 'desc',
},
skip: offset || 0,
take: limit || 10,
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});
return {
articles: articles.map((article: any) => articleMapper(article, id)),
articlesCount,
};
};
export const createArticle = async (article: any, id: number) => {
const { title, description, body, tagList } = article;
const tags = Array.isArray(tagList) ? tagList : [];
if (!title) {
throw new HttpException(422, { errors: { title: ["can't be blank"] } });
}
if (!description) {
throw new HttpException(422, { errors: { description: ["can't be blank"] } });
}
if (!body) {
throw new HttpException(422, { errors: { body: ["can't be blank"] } });
}
const slug = `${slugify(title)}-${id}`;
const existingTitle = await prisma.article.findUnique({
where: {
slug,
},
select: {
slug: true,
},
});
if (existingTitle) {
throw new HttpException(422, { errors: { title: ['must be unique'] } });
}
const {
authorId,
id: articleId,
...createdArticle
} = await prisma.article.create({
data: {
title,
description,
body,
slug,
tagList: {
connectOrCreate: tags.map((tag: string) => ({
create: { name: tag },
where: { name: tag },
})),
},
author: {
connect: {
id: id,
},
},
},
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});
return articleMapper(createdArticle, id);
};
export const getArticle = async (slug: string, id?: number) => {
const article = await prisma.article.findUnique({
where: {
slug,
},
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});
if (!article) {
throw new HttpException(404, { errors: { article: ['not found'] } });
}
return articleMapper(article, id);
};
const disconnectArticlesTags = async (slug: string) => {
await prisma.article.update({
where: {
slug,
},
data: {
tagList: {
set: [],
},
},
});
};
export const updateArticle = async (article: any, slug: string, id: number) => {
let newSlug = null;
const existingArticle = await await prisma.article.findFirst({
where: {
slug,
},
select: {
author: {
select: {
id: true,
username: true,
},
},
},
});
if (!existingArticle) {
throw new HttpException(404, {});
}
if (existingArticle.author.id !== id) {
throw new HttpException(403, {
message: 'You are not authorized to update this article',
});
}
if (article.title) {
newSlug = `${slugify(article.title)}-${id}`;
if (newSlug !== slug) {
const existingTitle = await prisma.article.findFirst({
where: {
slug: newSlug,
},
select: {
slug: true,
},
});
if (existingTitle) {
throw new HttpException(422, { errors: { title: ['must be unique'] } });
}
}
}
const tagList =
Array.isArray(article.tagList) && article.tagList?.length
? article.tagList.map((tag: string) => ({
create: { name: tag },
where: { name: tag },
}))
: [];
await disconnectArticlesTags(slug);
const updatedArticle = await prisma.article.update({
where: {
slug,
},
data: {
...(article.title ? { title: article.title } : {}),
...(article.body ? { body: article.body } : {}),
...(article.description ? { description: article.description } : {}),
...(newSlug ? { slug: newSlug } : {}),
updatedAt: new Date(),
tagList: {
connectOrCreate: tagList,
},
},
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});
return articleMapper(updatedArticle, id);
};
export const deleteArticle = async (slug: string, id: number) => {
const existingArticle = await await prisma.article.findFirst({
where: {
slug,
},
select: {
author: {
select: {
id: true,
username: true,
},
},
},
});
if (!existingArticle) {
throw new HttpException(404, {});
}
if (existingArticle.author.id !== id) {
throw new HttpException(403, {
message: 'You are not authorized to delete this article',
});
}
await prisma.article.delete({
where: {
slug,
},
});
};
export const getCommentsByArticle = async (slug: string, id?: number) => {
const queries = [];
queries.push({
author: {
demo: true,
},
});
if (id) {
queries.push({
author: {
id,
},
});
}
const comments = await prisma.article.findUnique({
where: {
slug,
},
include: {
comments: {
where: {
OR: queries,
},
select: {
id: true,
createdAt: true,
updatedAt: true,
body: true,
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
},
},
},
});
const result = comments?.comments.map((comment: any) => ({
...comment,
author: {
username: comment.author.username,
bio: comment.author.bio,
image: comment.author.image,
following: comment.author.followedBy.some((follow: any) => follow.id === id),
},
}));
return result;
};
export const addComment = async (body: string, slug: string, id: number) => {
if (!body) {
throw new HttpException(422, { errors: { body: ["can't be blank"] } });
}
const article = await prisma.article.findUnique({
where: {
slug,
},
select: {
id: true,
},
});
const comment = await prisma.comment.create({
data: {
body,
article: {
connect: {
id: article?.id,
},
},
author: {
connect: {
id: id,
},
},
},
include: {
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
},
});
return {
id: comment.id,
createdAt: comment.createdAt,
updatedAt: comment.updatedAt,
body: comment.body,
author: {
username: comment.author.username,
bio: comment.author.bio,
image: comment.author.image,
following: comment.author.followedBy.some((follow: any) => follow.id === id),
},
};
};
export const deleteComment = async (id: number, userId: number) => {
const comment = await prisma.comment.findFirst({
where: {
id,
author: {
id: userId,
},
},
select: {
author: {
select: {
id: true,
username: true,
},
},
},
});
if (!comment) {
throw new HttpException(404, {});
}
if (comment.author.id !== userId) {
throw new HttpException(403, {
message: 'You are not authorized to delete this comment',
});
}
await prisma.comment.delete({
where: {
id,
},
});
};
export const favoriteArticle = async (slugPayload: string, id: number) => {
const { _count, ...article } = await prisma.article.update({
where: {
slug: slugPayload,
},
data: {
favoritedBy: {
connect: {
id: id,
},
},
},
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});
const result = {
...article,
author: profileMapper(article.author, id),
tagList: article?.tagList.map((tag: Tag) => tag.name),
favorited: article.favoritedBy.some((favorited: any) => favorited.id === id),
favoritesCount: _count?.favoritedBy,
};
return result;
};
export const unfavoriteArticle = async (slugPayload: string, id: number) => {
const { _count, ...article } = await prisma.article.update({
where: {
slug: slugPayload,
},
data: {
favoritedBy: {
disconnect: {
id: id,
},
},
},
include: {
tagList: {
select: {
name: true,
},
},
author: {
select: {
username: true,
bio: true,
image: true,
followedBy: true,
},
},
favoritedBy: true,
_count: {
select: {
favoritedBy: true,
},
},
},
});
const result = {
...article,
author: profileMapper(article.author, id),
tagList: article?.tagList.map((tag: Tag) => tag.name),
favorited: article.favoritedBy.some((favorited: any) => favorited.id === id),
favoritesCount: _count?.favoritedBy,
};
return result;
};