Spaces:
Runtime error
Runtime error
| import { Readable } from "node:stream"; | |
| import type { CaseFile } from "@prisma/client"; | |
| import { | |
| MINIO_BUCKET_NAME, | |
| MINIO_PUBLIC_BASE_URL, | |
| } from "@/lib/constants"; | |
| import { assertCaseFileWithinSizeLimit, getSafeMimeType, isAllowedCaseFileType } from "@/lib/file-utils"; | |
| import { trimOrUndefined } from "@/lib/utils"; | |
| import { requireCaseByIdForOwner } from "@/server/cases/service"; | |
| import { requireCustomerByIdForOwner } from "@/server/customers/service"; | |
| import { db } from "@/server/db"; | |
| import { AppError } from "@/server/errors"; | |
| import { buildCaseFileStorageKey, ensureMinioBucket, minioClient } from "@/server/storage/minio"; | |
| import type { | |
| CaseFileListQueryInput, | |
| CaseFileMetadataInput, | |
| } from "@/server/validation/case"; | |
| import type { CaseFileRagStatusValue } from "@/lib/constants"; | |
| export function normalizeCaseFileListFilters(input: CaseFileListQueryInput) { | |
| return { | |
| search: trimOrUndefined(input.fileSearch), | |
| selectedFileId: trimOrUndefined(input.fileId), | |
| sessionPreset: input.sessionPreset, | |
| }; | |
| } | |
| export async function listCaseFilesForOwner( | |
| ownerId: string, | |
| customerId: string, | |
| caseId: string, | |
| search?: string, | |
| ) { | |
| await requireCaseByIdForOwner(ownerId, customerId, caseId); | |
| return db.caseFile.findMany({ | |
| where: { | |
| ownerId, | |
| customerId, | |
| caseId, | |
| ...(search | |
| ? { | |
| displayName: { | |
| contains: search, | |
| mode: "insensitive", | |
| }, | |
| } | |
| : {}), | |
| }, | |
| orderBy: { | |
| createdAt: "desc", | |
| }, | |
| }); | |
| } | |
| export async function listCustomerArchiveFilesForOwner( | |
| ownerId: string, | |
| customerId: string, | |
| search?: string, | |
| ) { | |
| await requireCustomerByIdForOwner(customerId, ownerId); | |
| return db.caseFile.findMany({ | |
| where: { | |
| ownerId, | |
| customerId, | |
| ...(search | |
| ? { | |
| displayName: { | |
| contains: search, | |
| mode: "insensitive", | |
| }, | |
| } | |
| : {}), | |
| }, | |
| include: { | |
| case: true, | |
| }, | |
| orderBy: { | |
| createdAt: "desc", | |
| }, | |
| }); | |
| } | |
| export async function getCaseFileByIdForOwner( | |
| ownerId: string, | |
| customerId: string, | |
| caseId: string, | |
| fileId: string, | |
| ) { | |
| return db.caseFile.findFirst({ | |
| where: { | |
| id: fileId, | |
| ownerId, | |
| customerId, | |
| caseId, | |
| }, | |
| include: { | |
| case: { | |
| include: { | |
| customer: true, | |
| }, | |
| }, | |
| }, | |
| }); | |
| } | |
| export async function requireCaseFileByIdForOwner( | |
| ownerId: string, | |
| customerId: string, | |
| caseId: string, | |
| fileId: string, | |
| ) { | |
| const file = await getCaseFileByIdForOwner(ownerId, customerId, caseId, fileId); | |
| if (!file) { | |
| throw new AppError("الملف ده مش موجود.", 404); | |
| } | |
| return file; | |
| } | |
| export async function uploadCaseFileForOwner( | |
| ownerId: string, | |
| customerId: string, | |
| caseId: string, | |
| metadata: CaseFileMetadataInput, | |
| uploadedFile: File, | |
| ) { | |
| await requireCaseByIdForOwner(ownerId, customerId, caseId); | |
| if (!uploadedFile || uploadedFile.size <= 0) { | |
| throw new AppError("اختار ملف قبل الرفع.", 400); | |
| } | |
| if (!assertCaseFileWithinSizeLimit(uploadedFile.size)) { | |
| throw new AppError("حجم الملف أكبر من الحد المسموح 25 ميجابايت.", 400); | |
| } | |
| const mimeType = getSafeMimeType(uploadedFile.name, uploadedFile.type); | |
| if (!isAllowedCaseFileType(uploadedFile.name, mimeType)) { | |
| throw new AppError("نوع الملف ده غير مدعوم في النسخة الحالية.", 400); | |
| } | |
| const arrayBuffer = await uploadedFile.arrayBuffer(); | |
| const buffer = Buffer.from(arrayBuffer); | |
| const storageKey = buildCaseFileStorageKey(caseId, uploadedFile.name); | |
| const ragStatus: CaseFileRagStatusValue = | |
| mimeType === "application/pdf" ? "PROCESSING" : "SKIPPED"; | |
| await ensureMinioBucket(); | |
| await minioClient.putObject(MINIO_BUCKET_NAME, storageKey, buffer, buffer.length, { | |
| "Content-Type": mimeType, | |
| }); | |
| return db.caseFile.create({ | |
| data: { | |
| ownerId, | |
| customerId, | |
| caseId, | |
| displayName: metadata.displayName.trim(), | |
| description: trimOrUndefined(metadata.description), | |
| originalFileName: uploadedFile.name, | |
| mimeType, | |
| sizeBytes: uploadedFile.size, | |
| storageBucket: MINIO_BUCKET_NAME, | |
| storageKey, | |
| ragStatus, | |
| }, | |
| }); | |
| } | |
| export async function updateCaseFileRagStatus( | |
| fileId: string, | |
| status: CaseFileRagStatusValue, | |
| options?: { | |
| error?: string | null; | |
| syncedAt?: Date | null; | |
| }, | |
| ) { | |
| return db.caseFile.update({ | |
| where: { | |
| id: fileId, | |
| }, | |
| data: { | |
| ragStatus: status, | |
| ragError: options?.error ?? null, | |
| ragSyncedAt: options?.syncedAt ?? null, | |
| }, | |
| }); | |
| } | |
| export async function listReadyPdfFilesForCase( | |
| ownerId: string, | |
| customerId: string, | |
| caseId: string, | |
| ) { | |
| await requireCaseByIdForOwner(ownerId, customerId, caseId); | |
| return db.caseFile.findMany({ | |
| where: { | |
| ownerId, | |
| customerId, | |
| caseId, | |
| mimeType: "application/pdf", | |
| ragStatus: "READY", | |
| }, | |
| orderBy: { | |
| createdAt: "desc", | |
| }, | |
| }); | |
| } | |
| export async function getCaseFileContentStream(file: CaseFile) { | |
| const stream = await minioClient.getObject(file.storageBucket, file.storageKey); | |
| return Readable.toWeb(stream) as ReadableStream<Uint8Array>; | |
| } | |
| export function buildCaseFileContentUrl(params: { | |
| customerId: string; | |
| caseId: string; | |
| fileId: string; | |
| download?: boolean; | |
| }) { | |
| const basePath = `/api/customers/${params.customerId}/cases/${params.caseId}/files/${params.fileId}/content`; | |
| return params.download ? `${basePath}?download=1` : basePath; | |
| } | |
| export function buildCaseFilePreviewUrl(params: { | |
| customerId: string; | |
| caseId: string; | |
| fileId: string; | |
| }) { | |
| const path = buildCaseFileContentUrl(params); | |
| if (!MINIO_PUBLIC_BASE_URL) { | |
| return path; | |
| } | |
| const normalizedBase = MINIO_PUBLIC_BASE_URL.replace(/\/$/, ""); | |
| return `${normalizedBase}${path}`; | |
| } | |
| export function buildInlineFileName(file: Pick<CaseFile, "displayName" | "originalFileName">) { | |
| const extension = file.originalFileName.includes(".") | |
| ? file.originalFileName.slice(file.originalFileName.lastIndexOf(".")) | |
| : ""; | |
| if (extension && file.displayName.toLowerCase().endsWith(extension.toLowerCase())) { | |
| return file.displayName; | |
| } | |
| return `${file.displayName}${extension}`; | |
| } | |