COURTRIX / src /server /files /service.ts
Ali-Developments's picture
Upload 125 files
ad79323 verified
Raw
History Blame Contribute Delete
6.36 kB
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}`;
}