Переработана схема привязки авторов к статьям, авторы трех типов.
This commit is contained in:
parent
35b470c498
commit
810599c718
@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AuthorRole" AS ENUM ('WRITER', 'PHOTOGRAPHER', 'EDITOR', 'TRANSLATOR');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Author" ADD COLUMN "roles" "AuthorRole"[] DEFAULT ARRAY[]::"AuthorRole"[];
|
@ -0,0 +1,17 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Article" DROP CONSTRAINT "Article_authorId_fkey";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ArticleAuthor" (
|
||||
"articleId" TEXT NOT NULL,
|
||||
"authorId" TEXT NOT NULL,
|
||||
"role" "AuthorRole" NOT NULL,
|
||||
|
||||
CONSTRAINT "ArticleAuthor_pkey" PRIMARY KEY ("articleId","authorId")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ArticleAuthor" ADD CONSTRAINT "ArticleAuthor_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ArticleAuthor" ADD CONSTRAINT "ArticleAuthor_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "Author"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `authorId` on the `Article` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Article" DROP COLUMN "authorId";
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `ArticleAuthor` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ArticleAuthor" DROP CONSTRAINT "ArticleAuthor_pkey",
|
||||
ADD CONSTRAINT "ArticleAuthor_pkey" PRIMARY KEY ("articleId", "authorId", "role");
|
@ -43,9 +43,9 @@ model Author {
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
articles Article[]
|
||||
isActive Boolean @default(true)
|
||||
roles AuthorRole[] @default([])
|
||||
articles ArticleAuthor[]
|
||||
}
|
||||
|
||||
model Article {
|
||||
@ -61,10 +61,20 @@ model Article {
|
||||
likes Int @default(0)
|
||||
dislikes Int @default(0)
|
||||
publishedAt DateTime @default(now())
|
||||
author Author @relation(fields: [authorId], references: [id])
|
||||
authorId String
|
||||
gallery GalleryImage[]
|
||||
isActive Boolean @default(false)
|
||||
authors ArticleAuthor[]
|
||||
}
|
||||
|
||||
model ArticleAuthor {
|
||||
articleId String
|
||||
authorId String
|
||||
role AuthorRole
|
||||
|
||||
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||
author Author @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([articleId, authorId, role])
|
||||
}
|
||||
|
||||
model GalleryImage {
|
||||
|
@ -7,7 +7,9 @@ import { AuthRequest } from '../../../middleware/auth';
|
||||
import { checkPermission } from '../../../utils/permissions';
|
||||
import { logger } from '../../../config/logger';
|
||||
import { Article } from "../../../types";
|
||||
import { bucketName, uploadBufferToS3 } from "../../../services/s3Service";
|
||||
import { uploadBufferToS3 } from "../../../services/s3Service";
|
||||
import { AuthorRole } from "@prisma/client";
|
||||
|
||||
|
||||
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
||||
|
||||
@ -15,14 +17,19 @@ export async function getArticle(req: Request, res: Response) : Promise<void> {
|
||||
try {
|
||||
const article = await prisma.article.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
authors: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
avatarUrl: true,
|
||||
email: true
|
||||
}
|
||||
email: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gallery: {
|
||||
orderBy: {order: "asc"}, // Сортировка изображений по порядку
|
||||
@ -95,6 +102,7 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
|
||||
return
|
||||
}
|
||||
|
||||
// создаём статью и добавляем автора с ролью WRITER
|
||||
const article = await prisma.article.create({
|
||||
data: {
|
||||
title,
|
||||
@ -104,17 +112,29 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
|
||||
cityId,
|
||||
coverImage,
|
||||
readTime,
|
||||
authorId: author.id
|
||||
publishedAt: new Date(),
|
||||
authors: {
|
||||
create: [
|
||||
{
|
||||
author: { connect: { id: author.id } },
|
||||
role: AuthorRole.WRITER, // 👈 по умолчанию WRITER
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
authors: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Создана статья: ${article.id} пользователем ${req.user.id}`);
|
||||
@ -127,21 +147,21 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
|
||||
|
||||
export async function updateArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||||
try {
|
||||
const { title, excerpt, content, categoryId, cityId, coverImage, readTime, author } = req.body;
|
||||
const { title, excerpt, content, categoryId, cityId, coverImage, readTime, authors } = req.body;
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||||
return
|
||||
}
|
||||
|
||||
const article = await prisma.article.findUnique({
|
||||
const existingArticle = await prisma.article.findUnique({
|
||||
where: { id: req.params.id },
|
||||
select: { authorId: true } // Берём только автора
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!article) {
|
||||
if (!existingArticle) {
|
||||
res.status(404).json({ error: 'Статья не найдена' });
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkPermission(req.user, categoryId, 'edit')) {
|
||||
@ -159,22 +179,34 @@ export async function updateArticle(req: AuthRequest, res: Response) : Promise<v
|
||||
readTime
|
||||
};
|
||||
|
||||
if (author?.id && author.id !== article.authorId) {
|
||||
updateData.author = { connect: { id: author.id } };
|
||||
// Обновляем список авторов, если он передан
|
||||
if (Array.isArray(authors)) {
|
||||
updateData.authors = {
|
||||
// Удалим всех существующих авторов и добавим заново
|
||||
deleteMany: {},
|
||||
create: authors.map((a ) => ({
|
||||
author: { connect: { id: a.author?.id } },
|
||||
role: a.role as AuthorRole
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const updatedArticle = await prisma.article.update({
|
||||
where: { id: req.params.id },
|
||||
data: updateData,
|
||||
include: {
|
||||
authors: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.json(updatedArticle);
|
||||
@ -207,14 +239,18 @@ export async function activeArticle(req: AuthRequest, res: Response) : Promise<v
|
||||
data: {
|
||||
isActive: isActive
|
||||
},
|
||||
include: {
|
||||
authors: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -241,14 +277,18 @@ export async function reactArticle(req: AuthRequest, res: Response) : Promise<vo
|
||||
likes: newLikes,
|
||||
dislikes: newDisLikes
|
||||
},
|
||||
include: {
|
||||
authors: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -315,7 +355,7 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
||||
|
||||
let importedCount = 0;
|
||||
|
||||
// Получение списка всех авторов для находдения автора по имени
|
||||
// Получение списка всех авторов для нахождения автора по имени
|
||||
const allAuthors = await prisma.author.findMany({
|
||||
select: { id: true, displayName: true },
|
||||
});
|
||||
@ -326,7 +366,34 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
||||
for (const article of articles) {
|
||||
try {
|
||||
// Шаг 1: Создание статьи
|
||||
const authorId = authorMap.get(article.authorName) || article.author.id;
|
||||
const authorsToConnect = [];
|
||||
|
||||
// Добавление писателя
|
||||
const writerId = authorMap.get(article.authorName);
|
||||
if (writerId) {
|
||||
authorsToConnect.push({
|
||||
author: { connect: { id: writerId } },
|
||||
role: AuthorRole.WRITER,
|
||||
});
|
||||
}
|
||||
|
||||
// Добавление соавтора
|
||||
const coWriterId = authorMap.get(article.coAuthorName);
|
||||
if (coWriterId) {
|
||||
authorsToConnect.push({
|
||||
author: { connect: { id: coWriterId } },
|
||||
role: AuthorRole.WRITER,
|
||||
});
|
||||
}
|
||||
|
||||
// Добавление фотографа
|
||||
const photographerId = authorMap.get(article.photographerName);
|
||||
if (photographerId) {
|
||||
authorsToConnect.push({
|
||||
author: { connect: { id: photographerId } },
|
||||
role: AuthorRole.PHOTOGRAPHER,
|
||||
});
|
||||
}
|
||||
|
||||
const newArticle = await prisma.article.create({
|
||||
data: {
|
||||
@ -341,8 +408,8 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
||||
publishedAt: new Date(article.publishedAt),
|
||||
likes: article.likes || 0,
|
||||
dislikes: article.dislikes || 0,
|
||||
author: {
|
||||
connect: { id: authorId },
|
||||
authors: {
|
||||
create: authorsToConnect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -33,13 +33,18 @@ export async function listArticles(req: Request, res: Response) {
|
||||
const [articles, total] = await Promise.all([
|
||||
prisma.article.findMany({
|
||||
where,
|
||||
include: {
|
||||
authors: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
avatarUrl: true,
|
||||
email: true
|
||||
email: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma } from '../../../lib/prisma';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { AuthorRole, Prisma } from '@prisma/client';
|
||||
|
||||
export async function searchArticles(req: Request, res: Response) {
|
||||
try {
|
||||
const { q, author, page = 1, limit = 9 } = req.query;
|
||||
const { q, author, role, page = 1, limit = 9 } = req.query;
|
||||
const skip = ((Number(page) || 1) - 1) * (Number(limit) || 9);
|
||||
|
||||
// Формируем where-условие
|
||||
@ -19,7 +19,16 @@ export async function searchArticles(req: Request, res: Response) {
|
||||
}
|
||||
: {}),
|
||||
...(typeof author === 'string' && author.trim()
|
||||
? { authorId: author }
|
||||
? {
|
||||
authors: {
|
||||
some: {
|
||||
authorId: author,
|
||||
...(typeof role === 'string' && role.trim()
|
||||
? { role: role as AuthorRole } // 👈 проверяем и добавляем роль
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
@ -27,6 +36,8 @@ export async function searchArticles(req: Request, res: Response) {
|
||||
const [articles, total] = await Promise.all([
|
||||
prisma.article.findMany({
|
||||
where,
|
||||
include: {
|
||||
authors: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
@ -34,6 +45,9 @@ export async function searchArticles(req: Request, res: Response) {
|
||||
displayName: true,
|
||||
avatarUrl: true,
|
||||
email: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -49,6 +63,7 @@ export async function searchArticles(req: Request, res: Response) {
|
||||
articles,
|
||||
totalPages: Math.ceil(total / (Number(limit) || 9)),
|
||||
currentPage: Number(page) || 1,
|
||||
total: total,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска по статьям:', error);
|
||||
|
@ -3,11 +3,13 @@ import { AuthRequest } from '../../../middleware/auth';
|
||||
import { authorService } from '../../../services/authorService';
|
||||
import { logger } from "../../../config/logger";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { AuthorRole } from "@prisma/client";
|
||||
|
||||
|
||||
export async function getAuthors(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const authors = await authorService.getAuthors();
|
||||
const { role, page } = req.query;
|
||||
const authors = await authorService.getAuthors(Number(page), role as string);
|
||||
res.json(authors);
|
||||
} catch {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
@ -22,7 +24,14 @@ export async function createAuthor(req: AuthRequest, res: Response): Promise<voi
|
||||
return
|
||||
}
|
||||
|
||||
const { email, displayName, bio, avatarUrl, order } = req.body;
|
||||
const { email, displayName, bio, avatarUrl, okUrl, vkUrl, websiteUrl, roles } = req.body;
|
||||
|
||||
// Проверим, что пришедшие роли валидны
|
||||
const validRoles = Array.isArray(roles)
|
||||
? roles.filter((role: any): role is AuthorRole =>
|
||||
Object.values(AuthorRole).includes(role as AuthorRole)
|
||||
)
|
||||
: [];
|
||||
|
||||
const lastAuthor = await prisma.author.findFirst({
|
||||
orderBy: {
|
||||
@ -38,7 +47,11 @@ export async function createAuthor(req: AuthRequest, res: Response): Promise<voi
|
||||
displayName,
|
||||
bio,
|
||||
avatarUrl,
|
||||
order: nextOrder
|
||||
order: nextOrder,
|
||||
okUrl,
|
||||
vkUrl,
|
||||
websiteUrl,
|
||||
roles: validRoles,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@ -47,6 +60,10 @@ export async function createAuthor(req: AuthRequest, res: Response): Promise<voi
|
||||
bio: true,
|
||||
avatarUrl: true,
|
||||
order: true,
|
||||
okUrl: true,
|
||||
vkUrl: true,
|
||||
websiteUrl: true,
|
||||
roles: true,
|
||||
}
|
||||
});
|
||||
|
||||
@ -67,7 +84,14 @@ export async function updateAuthor(req: AuthRequest, res: Response): Promise<voi
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { email, displayName, bio, avatarUrl, order, okUrl, vkUrl, websiteUrl } = req.body;
|
||||
const { email, displayName, bio, avatarUrl, order, okUrl, vkUrl, websiteUrl, roles } = req.body;
|
||||
|
||||
// Проверим, что пришедшие роли валидны
|
||||
const validRoles = Array.isArray(roles)
|
||||
? roles.filter((role: any): role is AuthorRole =>
|
||||
Object.values(AuthorRole).includes(role as AuthorRole)
|
||||
)
|
||||
: [];
|
||||
|
||||
// Подготовка данных для изменения
|
||||
const updateData: any = {
|
||||
@ -79,6 +103,7 @@ export async function updateAuthor(req: AuthRequest, res: Response): Promise<voi
|
||||
okUrl,
|
||||
vkUrl,
|
||||
websiteUrl,
|
||||
roles: validRoles,
|
||||
};
|
||||
|
||||
// Обновление данных автора
|
||||
@ -95,6 +120,7 @@ export async function updateAuthor(req: AuthRequest, res: Response): Promise<voi
|
||||
okUrl: true,
|
||||
vkUrl: true,
|
||||
websiteUrl: true,
|
||||
roles: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -6,7 +6,8 @@ import { AuthRequest } from "../../../middleware/auth";
|
||||
// Изменить количество лайков
|
||||
export async function updateLikes(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const authors = await authorService.getAuthors();
|
||||
const { role, page } = req.query;
|
||||
const authors = await authorService.getAuthors(Number(page), role as string);
|
||||
res.json(authors);
|
||||
} catch {
|
||||
res.status(500).json({ error: 'Серверная ошибка' });
|
||||
|
@ -1,10 +1,29 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthorRole } from '@prisma/client';
|
||||
import { Author } from '../types/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const authorService = {
|
||||
getAuthors: async (): Promise<Author[]> => {
|
||||
getAuthors: async (page: number, role?: string): Promise<Author[]> => {
|
||||
|
||||
if (role && !Object.values(AuthorRole).includes(role as AuthorRole)) {
|
||||
throw new Error(`Недопустимая роль: ${role}`);
|
||||
}
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
const sortRoles = role
|
||||
? {
|
||||
roles: {
|
||||
has: role as AuthorRole, // Prisma оператор для enum[]
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
const skip = page ? (page - 1) * pageSize : undefined;
|
||||
const take = page ? pageSize : undefined;
|
||||
|
||||
try {
|
||||
// 1. Получаем авторов
|
||||
const authors = await prisma.author.findMany({
|
||||
@ -20,8 +39,15 @@ export const authorService = {
|
||||
email: true,
|
||||
userId: true,
|
||||
isActive: true,
|
||||
roles: true,
|
||||
_count: {
|
||||
select: {articles: true}, // Подсчёт количества статей автора
|
||||
select: {
|
||||
articles: {
|
||||
where: {
|
||||
role: role as AuthorRole, // 👈 учитываются только WRITER-авторы
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
@ -29,24 +55,28 @@ export const authorService = {
|
||||
},
|
||||
}
|
||||
},
|
||||
where: {order: {not: 0}},
|
||||
where: {
|
||||
order: {not: 0},
|
||||
...sortRoles, // 👈 добавляем условие по ролям
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
|
||||
const sql = `
|
||||
SELECT "authorId", SUM("Article"."likes") AS "totalLikes"
|
||||
FROM "ArticleAuthor"
|
||||
JOIN "Article" ON "ArticleAuthor"."articleId" = "Article"."id"
|
||||
WHERE "ArticleAuthor"."role" = $1::"AuthorRole"
|
||||
GROUP BY "authorId"
|
||||
`;
|
||||
|
||||
// 2. Считаем сумму лайков по авторам
|
||||
const likesPerAuthor = await prisma.article.groupBy({
|
||||
by: ['authorId'],
|
||||
_sum: {
|
||||
likes: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 3. Строим map для быстрого доступа
|
||||
const likesMap = new Map(
|
||||
likesPerAuthor.map((item) => [item.authorId, item._sum.likes ?? 0])
|
||||
);
|
||||
const rawLikes: { authorId: string; totalLikes: number | null }[] = await prisma.$queryRawUnsafe(sql, role);
|
||||
const likesMap = new Map(rawLikes.map(item => [item.authorId, item.totalLikes ?? 0]));
|
||||
|
||||
return authors.map(author => ({
|
||||
id: author.id,
|
||||
@ -62,7 +92,8 @@ export const authorService = {
|
||||
userId: author.userId,
|
||||
userDisplayName: author.user?.displayName || null,
|
||||
isActive: author.isActive,
|
||||
totalLikes: likesMap.get(author.id) ?? 0,
|
||||
roles: author.roles,
|
||||
totalLikes: Number(likesMap.get(author.id) ?? 0),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения авторов:', error);
|
||||
|
@ -10,6 +10,8 @@ export interface Article {
|
||||
cityId: number;
|
||||
author: Author;
|
||||
authorName: string;
|
||||
coAuthorName: string;
|
||||
photographerName: string;
|
||||
coverImage: string;
|
||||
images?: string[];
|
||||
imageSubs: string[];
|
||||
|
Loading…
x
Reference in New Issue
Block a user