/** * /database/api/* 路由共享件:依赖装配、actor 门禁、统一错误映射。 * 约定(与 admin 面一致):绝对路径;guard 前置 fail closed;查询 scope 到 silo org。 */ import type { FastifyReply, FastifyRequest } from "fastify"; import { Prisma } from "@prisma/client"; import type { PrismaClient } from "@prisma/client"; import { FileLibError } from "./model.js"; import { requireFileLibActor } from "./guards.js"; import type { FileLibActor, TreeServiceDeps } from "./treeService.js"; import type { GroupResolver } from "./groupResolver.js"; import type { VersionStore } from "./versionStore.js"; import type { ExportAdapter } from "./exportService.js"; export interface FileLibRouteDeps { readonly prisma: PrismaClient; readonly sessionSecret: string; readonly organizationId: string; readonly storageRoot: string; readonly groupResolver: GroupResolver; readonly versionStore: VersionStore; readonly exportAdapters: readonly ExportAdapter[]; } /** 组装 treeService 依赖(路由处理内直接使用)。 */ export function treeDeps(deps: FileLibRouteDeps): TreeServiceDeps { return { prisma: deps.prisma, groupResolver: deps.groupResolver, versionStore: deps.versionStore, organizationId: deps.organizationId, storageRoot: deps.storageRoot, }; } /** 端点第一行调用;null = 响应已发(401/403),fail closed。 */ export async function actorOrNull( request: FastifyRequest, reply: FastifyReply, deps: FileLibRouteDeps, ): Promise { return requireFileLibActor(request, reply, { prisma: deps.prisma, sessionSecret: deps.sessionSecret, organizationId: deps.organizationId, }); } /** 统一错误出口:FileLibError → 语义码;P2002 → 409;其余 → 500(不泄露内部)。 */ export async function sendRouteError(reply: FastifyReply, error: unknown): Promise { if (error instanceof FileLibError) { await reply.status(error.statusCode).send({ error: { code: error.code, message: error.message, ...(error.details ?? {}) }, }); return; } if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { await reply.status(409).send({ error: { code: "conflict", message: "uniqueness conflict" } }); return; } reply.log.error({ err: error }, "filelib route: unexpected error"); await reply.status(500).send({ error: { code: "internal", message: "internal error" } }); } /** 请求体轻量校验(抛 FileLibError 400)。 */ export function bodyObject(body: unknown): Record { if (typeof body !== "object" || body === null || Array.isArray(body)) { throw new FileLibError(400, "invalid_request", "request body must be a JSON object"); } return body as Record; } export function requireString(obj: Record, key: string): string { const value = obj[key]; if (typeof value !== "string" || value === "") { throw new FileLibError(400, "invalid_request", `missing or invalid field: ${key}`); } return value; } export function optionalString(obj: Record, key: string): string | undefined { const value = obj[key]; if (value === undefined || value === null) return undefined; if (typeof value !== "string") { throw new FileLibError(400, "invalid_request", `invalid field: ${key}`); } return value; }