feat(po-attachments): add upload route for PO additional attachments
This commit is contained in:
parent
a16b8fcdcb
commit
b362d92256
1 changed files with 111 additions and 0 deletions
111
src/app/api/v1/purchase-orders/[id]/attachments/upload/route.ts
Normal file
111
src/app/api/v1/purchase-orders/[id]/attachments/upload/route.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
|
||||
|
||||
/**
|
||||
* POST /api/v1/purchase-orders/[id]/attachments/upload
|
||||
*
|
||||
* Upload an additional file (supplier quote, drawing, NDA, etc.) to
|
||||
* a PO. Sits alongside the versioned PDF in `documents`. Goes
|
||||
* through admin PB so the operator doesn't need a PocketBase account
|
||||
* with the createRule cleared.
|
||||
*
|
||||
* Body: multipart/form-data with a single `file` field.
|
||||
*
|
||||
* Response:
|
||||
* 201 — { ok: true, attachments: [token, ...] }
|
||||
* 400 — { error: "No file" }
|
||||
* 413 — { error: "File too large (max 25 MB)" }
|
||||
* 500 — { error: "PB admin unavailable" | <message> }
|
||||
*
|
||||
* Cap: 25 MB per file to match the rest of the app. n8n's outbound
|
||||
* SMTP layer has its own limits; staying under 25 MB is safe.
|
||||
*/
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const MAX_BYTES = 25 * 1024 * 1024;
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id: poId } = await params;
|
||||
|
||||
let formData: FormData;
|
||||
try {
|
||||
formData = await req.formData();
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{ error: err instanceof Error ? err.message : "Invalid form data" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const file = formData.get("file");
|
||||
if (!file || typeof file === "string" || typeof (file as File).name !== "string" || typeof (file as File).size !== "number") {
|
||||
return NextResponse.json(
|
||||
{ error: "No file in request (expected field 'file')" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (file.size > MAX_BYTES) {
|
||||
return NextResponse.json(
|
||||
{ error: `File too large (max ${MAX_BYTES / 1024 / 1024} MB)` },
|
||||
{ status: 413 },
|
||||
);
|
||||
}
|
||||
|
||||
let adminPb;
|
||||
try {
|
||||
adminPb = await getAdminPocketBase();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "PB admin unavailable" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the current attachments array, append the new file's
|
||||
// token, write back. PB's FormData update replaces the field,
|
||||
// so we need the read-modify-write — there is no "append" op
|
||||
// on a multi-file field via the SDK.
|
||||
const po = await adminPb
|
||||
.collection("purchase_orders")
|
||||
.getOne<{ additional_attachments?: string[] | string }>(poId, {
|
||||
fields: "id,additional_attachments",
|
||||
});
|
||||
const currentRaw = po?.additional_attachments ?? [];
|
||||
const current = Array.isArray(currentRaw) ? currentRaw : currentRaw ? [currentRaw] : [];
|
||||
|
||||
const updateFd = new FormData();
|
||||
updateFd.append("file", file);
|
||||
await adminPb.collection("purchase_orders").update(poId, updateFd);
|
||||
|
||||
// Re-read to get the actual token PB stored (FormData only
|
||||
// carries the operator's filename; PB slug-normalises on disk).
|
||||
const after = await adminPb
|
||||
.collection("purchase_orders")
|
||||
.getOne<{ additional_attachments?: string[] | string }>(poId, {
|
||||
fields: "additional_attachments",
|
||||
});
|
||||
const afterRaw = after?.additional_attachments ?? [];
|
||||
const afterArr = Array.isArray(afterRaw) ? afterRaw : afterRaw ? [afterRaw] : [];
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: true,
|
||||
attachments: afterArr,
|
||||
added: afterArr.filter((t) => !current.includes(t)),
|
||||
},
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{ error: err instanceof Error ? err.message : "Upload failed" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue