import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import type { CreateWebsiteBlogCategoryDto, CreateWebsiteBlogPostDto, } from './dto/website-blog.dto'; @Injectable() export class WebsiteBlogService { constructor(private readonly prisma: PrismaService) {} async listCategories() { const items = await this.prisma.websiteBlogCategory.findMany({ orderBy: { name: 'asc' }, }); return { items }; } async createCategory(dto: CreateWebsiteBlogCategoryDto) { const name = dto.name.trim(); if (!name) { throw new BadRequestException('Enter a category name.'); } const parentId = dto.parentId?.trim() ? this.parseId(dto.parentId.trim()) : null; if (parentId) { const parent = await this.prisma.websiteBlogCategory.findUnique({ where: { id: parentId }, }); if (!parent) { throw new NotFoundException('Parent category not found.'); } } const existing = await this.prisma.websiteBlogCategory.findFirst({ where: { parentId, name: { equals: name, mode: 'insensitive' }, }, }); if (existing) { throw new BadRequestException('This category already exists here.'); } try { return await this.prisma.websiteBlogCategory.create({ data: { name, nameFa: dto.nameFa?.trim() || null, description: dto.description?.trim() || null, parentId: parentId ?? null, }, }); } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002' ) { throw new BadRequestException('This category already exists here.'); } throw error; } } async updateCategory(id: string, dto: CreateWebsiteBlogCategoryDto) { const categoryId = this.parseId(id); const current = await this.prisma.websiteBlogCategory.findUnique({ where: { id: categoryId }, }); if (!current) { throw new NotFoundException('Category not found.'); } const name = dto.name.trim(); if (!name) { throw new BadRequestException('Enter a category name.'); } const parentId = dto.parentId?.trim() ? this.parseId(dto.parentId.trim()) : null; if (parentId === categoryId) { throw new BadRequestException('A category cannot be its own parent.'); } const rows = await this.prisma.websiteBlogCategory.findMany({ select: { id: true, parentId: true }, }); if (parentId) { const descendants = this.descendantIds(categoryId, rows); if (descendants.has(parentId.toString())) { throw new BadRequestException( 'Choose a parent outside this category.', ); } const parent = rows.find((row) => row.id === parentId); if (!parent) { throw new NotFoundException('Parent category not found.'); } } const existing = await this.prisma.websiteBlogCategory.findFirst({ where: { parentId, name: { equals: name, mode: 'insensitive' }, NOT: { id: categoryId }, }, }); if (existing) { throw new BadRequestException('This category already exists here.'); } return this.prisma.websiteBlogCategory.update({ where: { id: categoryId }, data: { name, nameFa: dto.nameFa?.trim() || null, description: dto.description?.trim() || null, parentId, }, }); } async deleteCategory(id: string) { const rootId = this.parseId(id); const rows = await this.prisma.websiteBlogCategory.findMany({ select: { id: true, parentId: true }, }); if (!rows.some((row) => row.id === rootId)) { throw new NotFoundException('Category not found.'); } const subtree = this.subtreePostOrder(rootId, rows); const posts = await this.prisma.websiteBlogPost.count({ where: { categoryId: { in: subtree } }, }); if (posts > 0) { throw new BadRequestException( 'Remove posts in this category before deleting it.', ); } for (const categoryId of subtree) { await this.prisma.websiteBlogCategory.delete({ where: { id: categoryId } }); } return { ok: true }; } async listPosts() { const posts = await this.prisma.websiteBlogPost.findMany({ orderBy: { createdAt: 'desc' }, include: { category: true }, }); return { total: posts.length, items: posts.map((post) => ({ id: post.id, title: post.title, abstract: post.abstract, imageUrl: post.imageUrl, categoryId: post.categoryId, categoryName: post.category.name, tags: post.tags, createdAt: post.createdAt, updatedAt: post.updatedAt, })), }; } async createPost(dto: CreateWebsiteBlogPostDto) { const categoryId = this.parseId(dto.categoryId); const category = await this.prisma.websiteBlogCategory.findUnique({ where: { id: categoryId }, }); if (!category) { throw new NotFoundException('Category not found.'); } const post = await this.prisma.websiteBlogPost.create({ data: { title: dto.title.trim(), abstract: dto.abstract?.trim() || null, mainTextHtml: dto.mainTextHtml?.trim() || null, imageUrl: dto.imageUrl?.trim() || null, categoryId, tags: this.normalizeTags(dto.tags), }, include: { category: true }, }); return { id: post.id, title: post.title, abstract: post.abstract, mainTextHtml: post.mainTextHtml, imageUrl: post.imageUrl, categoryId: post.categoryId, categoryName: post.category.name, tags: post.tags, createdAt: post.createdAt, updatedAt: post.updatedAt, }; } async deletePost(id: string) { const postId = this.parseId(id); try { await this.prisma.websiteBlogPost.delete({ where: { id: postId } }); } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025' ) { throw new NotFoundException('Blog post not found.'); } throw error; } return { ok: true }; } private descendantIds( rootId: bigint, rows: { id: bigint; parentId: bigint | null }[], ) { const childrenByParent = new Map(); for (const row of rows) { if (row.parentId == null) continue; const key = row.parentId.toString(); const list = childrenByParent.get(key) ?? []; list.push(row.id); childrenByParent.set(key, list); } const result = new Set(); const queue = [...(childrenByParent.get(rootId.toString()) ?? [])]; while (queue.length) { const current = queue.shift()!; const key = current.toString(); if (result.has(key)) continue; result.add(key); queue.push(...(childrenByParent.get(key) ?? [])); } return result; } private subtreePostOrder( rootId: bigint, rows: { id: bigint; parentId: bigint | null }[], ) { const childrenByParent = new Map(); for (const row of rows) { if (row.parentId == null) continue; const key = row.parentId.toString(); const list = childrenByParent.get(key) ?? []; list.push(row.id); childrenByParent.set(key, list); } const order: bigint[] = []; const walk = (current: bigint) => { for (const child of childrenByParent.get(current.toString()) ?? []) { walk(child); } order.push(current); }; walk(rootId); return order; } private parseId(value: string) { if (!/^\d+$/.test(value)) { throw new BadRequestException('Invalid id.'); } return BigInt(value); } private normalizeTags(tags: string[] | undefined) { const seen = new Set(); const next: string[] = []; for (const tag of tags ?? []) { const trimmed = tag.trim(); if (!trimmed) continue; const key = trimmed.toLowerCase(); if (seen.has(key)) continue; seen.add(key); next.push(trimmed); } return next; } }