src/lib/documents/storage.ts
Linked repository file from this build.
import { z } from "zod";
import { RuleError } from "@/lib/errors";
export const MAX_PDF_BYTES = 10 * 1024 * 1024;
export interface DocumentStorage {
put(file: File): Promise<string>;
url(key: string): string;
}
export async function validatePdf(file: File): Promise<void> {
if (file.size > MAX_PDF_BYTES) throw new RuleError("PDF must be 10 MB or smaller.");
if (!file.size) throw new RuleError("The PDF is empty.");
if (
!file.name.toLowerCase().endsWith(".pdf") ||
(file.type !== "application/pdf" && file.type !== "")
)
throw new RuleError("Select a PDF file.");
if (new TextDecoder().decode(await file.slice(0, 5).arrayBuffer()) !== "%PDF-")
throw new RuleError("The file does not have a PDF header.");
}
/** Upload through the configured server adapter. Review metadata stays in Postgres. */
export function createDocumentStorage(): DocumentStorage {
return {
async put(file) {
await validatePdf(file);
const start = await fetch("/api/documents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: file.name, size: file.size }),
});
const upload = await readUploadResponse(start, uploadStartSchema);
if (upload.mode === "local") {
const form = new FormData();
form.append("file", file);
return (
await readUploadResponse(
await fetch("/api/documents", { method: "POST", body: form }),
uploadCompleteSchema,
)
).key;
}
const form = new FormData();
for (const [name, value] of Object.entries(upload.fields)) form.append(name, value);
form.append("file", file);
const uploaded = await fetch(upload.url, { method: "POST", body: form, credentials: "omit" });
if (!uploaded.ok) throw new RuleError("Unable to upload PDF. Try again.");
const completed = await fetch("/api/documents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uploadId: upload.uploadId }),
});
return (await readUploadResponse(completed, uploadCompleteSchema)).key;
},
url(key) {
return `/api/documents/${key}`;
},
};
}
const uploadStartSchema = z.discriminatedUnion("mode", [
z.object({ mode: z.literal("local") }),
z.object({
mode: z.literal("neon"),
uploadId: z.uuid(),
url: z.url(),
fields: z.record(z.string(), z.string()),
}),
]);
const uploadCompleteSchema = z.object({ key: z.uuid() });
async function readUploadResponse<T>(response: Response, schema: z.ZodType<T>): Promise<T> {
const data: unknown = await response.json().catch(() => null);
if (!response.ok) {
const error = z.object({ error: z.string() }).safeParse(data);
throw new RuleError(error.success ? error.data.error : "Unable to save PDF. Try again.");
}
const parsed = schema.safeParse(data);
if (!parsed.success) throw new RuleError("Invalid upload response. Try again.");
return parsed.data;
}