import { BadRequestException, ForbiddenException, HttpException, } from '@nestjs/common'; import type { ContentStatus } from '@doodle/contracts'; import { Prisma } from '../generated/prisma/client'; import { requireCurrentAccount } from '../auth/account-policy'; import type { AuthenticatedUser } from '../common/http-request'; import { ApiException } from '../common/api-exception'; /** 只从 active 根节点展开;草稿祖先、失联或成环的分支均不对外可见。 */ export const visibleCategoryIds = Prisma.sql`WITH RECURSIVE visible_categories AS ( SELECT id FROM public.categories WHERE parent_id IS NULL AND status = 'active' UNION SELECT c.id FROM public.categories c JOIN visible_categories p ON c.parent_id = p.id WHERE c.status = 'active' ) SELECT id FROM visible_categories`; /** 题型列表、详情和管理预览共用规则,不能只过滤左侧分类菜单。SQL 中题型别名固定为 w。 */ export const visibleWorksheet = Prisma.sql`w.status = 'active' AND w.category_id IN (${visibleCategoryIds})`; /** 发布状态显式校验;不接受布尔值、NULL 或客户端自行添加的测试状态。 */ export function contentStatus(value: unknown): ContentStatus { if (value !== 'draft' && value !== 'active' && value !== 'hidden') throw new BadRequestException('发布状态不合法'); return value; } /** 写事务重新核对当前账户并持锁,避免守卫之后被禁用或降权仍然提交。 */ export async function lockContentAdmin( tx: Prisma.TransactionClient, actor: AuthenticatedUser, ): Promise { const rows = await tx.$queryRaw< Array<{ id: string; status: 'active' | 'disabled'; role: 'user' | 'admin'; authVersion: number; }> >`SELECT id, status, role, auth_version AS "authVersion" FROM public.users WHERE id = ${actor.userId} FOR UPDATE`; const account = requireCurrentAccount( rows[0] ?? null, actor.userId, actor.authVersion, ); if (account.role !== 'admin') throw new ForbiddenException(); } /** 先撤销数据库展示快照与发布标记;外部已分发的静态文件须由后续发布链路撤销/重建。 */ export async function invalidateContentSnapshots( tx: Prisma.TransactionClient, ): Promise { await tx.pageConfig.updateMany({ data: { home: Prisma.DbNull, category: Prisma.DbNull, age: Prisma.DbNull, version: { increment: 1 }, publishedVersion: null, publishedAt: null, updatedAt: new Date(), }, }); } /** 保留业务 HTTP 错误;连接/约束诊断不原样输出给调用者。 */ export async function runContentQuery( operation: () => Promise, ): Promise { try { return await operation(); } catch (error) { if (error instanceof HttpException) throw error; throw new ApiException(503, 'DATABASE_UNAVAILABLE'); } }