Import source: app/backend/src/routes/lists
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,156 @@
|
|||||||
|
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { prisma } from '../db.js';
|
||||||
|
import { requireAuth } from '../plugins/session.js';
|
||||||
|
|
||||||
|
const listBodySchema = z.object({
|
||||||
|
name: z.string().trim().min(2).max(80),
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemBodySchema = z.object({
|
||||||
|
articleId: z.string().trim().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const reorderBodySchema = z.object({
|
||||||
|
itemIds: z.array(z.string().trim().min(1)).max(200),
|
||||||
|
});
|
||||||
|
|
||||||
|
function faDate(date: Date) {
|
||||||
|
return date.toLocaleDateString('fa-IR');
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicArticle(article: any) {
|
||||||
|
const publishedAt = article.publishedAt ?? article.createdAt;
|
||||||
|
return {
|
||||||
|
id: article.id,
|
||||||
|
slug: article.slug,
|
||||||
|
title: article.title,
|
||||||
|
summary: article.summary,
|
||||||
|
type: article.type,
|
||||||
|
status: article.status,
|
||||||
|
readTime: article.readTime,
|
||||||
|
views: article.views,
|
||||||
|
category: article.category?.slug,
|
||||||
|
categoryName: article.category?.name,
|
||||||
|
authorId: article.author?.id,
|
||||||
|
authorSlug: article.author?.slug,
|
||||||
|
authorName: article.author?.name,
|
||||||
|
authorUsername: article.author?.user?.username ?? null,
|
||||||
|
date: faDate(publishedAt),
|
||||||
|
dateEn: publishedAt.toISOString().slice(0, 10),
|
||||||
|
tags: article.tags?.map((t: any) => t.tag.name) ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicList(row: any) {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
isDefault: row.isDefault,
|
||||||
|
createdAt: row.createdAt.toISOString(),
|
||||||
|
items: (row.items ?? []).map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
position: item.position,
|
||||||
|
addedAt: item.addedAt.toISOString(),
|
||||||
|
article: publicArticle(item.article),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList(id: string, userId: string) {
|
||||||
|
return prisma.readingList.findFirst({
|
||||||
|
where: { id, userId },
|
||||||
|
include: {
|
||||||
|
items: {
|
||||||
|
include: {
|
||||||
|
article: {
|
||||||
|
include: {
|
||||||
|
category: true,
|
||||||
|
author: { include: { user: { select: { username: true } } } },
|
||||||
|
tags: { include: { tag: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ position: 'asc' }, { addedAt: 'asc' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function listRoutes(app: FastifyInstance) {
|
||||||
|
app.get('/api/v1/auth/lists', { preHandler: requireAuth() }, async (req) => {
|
||||||
|
const rows = await prisma.readingList.findMany({
|
||||||
|
where: { userId: req.session!.userId },
|
||||||
|
include: {
|
||||||
|
items: {
|
||||||
|
include: {
|
||||||
|
article: {
|
||||||
|
include: {
|
||||||
|
category: true,
|
||||||
|
author: { include: { user: { select: { username: true } } } },
|
||||||
|
tags: { include: { tag: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ position: 'asc' }, { addedAt: 'asc' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ isDefault: 'desc' }, { createdAt: 'asc' }],
|
||||||
|
});
|
||||||
|
return { items: rows.map(publicList) };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/auth/lists', { preHandler: requireAuth(), schema: { body: { type: 'object', additionalProperties: true } } }, async (req, reply) => {
|
||||||
|
const parsed = listBodySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' });
|
||||||
|
const row = await prisma.readingList.create({
|
||||||
|
data: { userId: req.session!.userId, name: parsed.data.name },
|
||||||
|
}).catch(() => null);
|
||||||
|
if (!row) return reply.code(409).send({ error: 'list_exists' });
|
||||||
|
return { list: publicList({ ...row, items: [] }) };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete<{ Params: { id: string } }>('/api/v1/auth/lists/:id', { preHandler: requireAuth() }, async (req, reply) => {
|
||||||
|
const row = await prisma.readingList.findFirst({ where: { id: req.params.id, userId: req.session!.userId } });
|
||||||
|
if (!row) return reply.code(404).send({ error: 'not_found' });
|
||||||
|
if (row.isDefault) return reply.code(409).send({ error: 'default_list_locked' });
|
||||||
|
await prisma.readingList.delete({ where: { id: row.id } });
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post<{ Params: { id: string }; Body: unknown }>('/api/v1/auth/lists/:id/items', { preHandler: requireAuth(), schema: { body: { type: 'object', additionalProperties: true } } }, async (req, reply) => {
|
||||||
|
const parsed = itemBodySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' });
|
||||||
|
const list = await prisma.readingList.findFirst({ where: { id: req.params.id, userId: req.session!.userId } });
|
||||||
|
if (!list) return reply.code(404).send({ error: 'not_found' });
|
||||||
|
const article = await prisma.article.findUnique({ where: { id: parsed.data.articleId } });
|
||||||
|
if (!article) return reply.code(404).send({ error: 'article_not_found' });
|
||||||
|
const nextPosition = await prisma.readingListItem.count({ where: { listId: list.id } });
|
||||||
|
await prisma.readingListItem.upsert({
|
||||||
|
where: { listId_articleId: { listId: list.id, articleId: article.id } },
|
||||||
|
update: {},
|
||||||
|
create: { listId: list.id, articleId: article.id, position: nextPosition + 1 },
|
||||||
|
});
|
||||||
|
const fresh = await loadList(list.id, req.session!.userId);
|
||||||
|
return { list: publicList(fresh) };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete<{ Params: { id: string; itemId: string } }>('/api/v1/auth/lists/:id/items/:itemId', { preHandler: requireAuth() }, async (req, reply) => {
|
||||||
|
const list = await prisma.readingList.findFirst({ where: { id: req.params.id, userId: req.session!.userId } });
|
||||||
|
if (!list) return reply.code(404).send({ error: 'not_found' });
|
||||||
|
await prisma.readingListItem.deleteMany({ where: { id: req.params.itemId, listId: list.id } });
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put<{ Params: { id: string }; Body: unknown }>('/api/v1/auth/lists/:id/reorder', { preHandler: requireAuth(), schema: { body: { type: 'object', additionalProperties: true } } }, async (req, reply) => {
|
||||||
|
const parsed = reorderBodySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' });
|
||||||
|
const list = await prisma.readingList.findFirst({ where: { id: req.params.id, userId: req.session!.userId } });
|
||||||
|
if (!list) return reply.code(404).send({ error: 'not_found' });
|
||||||
|
await prisma.$transaction(parsed.data.itemIds.map((id, index) => (
|
||||||
|
prisma.readingListItem.updateMany({ where: { id, listId: list.id }, data: { position: index + 1 } })
|
||||||
|
)));
|
||||||
|
const fresh = await loadList(list.id, req.session!.userId);
|
||||||
|
return { list: publicList(fresh) };
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user