/** * One-shot migration: broaden the `quote_attachments.file` mime-type * allowlist to include `text/csv`, `text/plain`, and * `application/octet-stream`, so senders that attach CSV / plain-text / * unknown-binary files (zip, dwg, etc.) don't get silently dropped at * the PocketBase validation layer. * * Idempotent: re-running is a no-op (just rewrites the same allowlist). * * Usage: tsx scripts/migrations/broaden-quote-attachments-mime-types.ts */ import PocketBase from "pocketbase"; const PB_URL = process.env.PB_URL || "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io"; const ADMIN_EMAIL = process.env.POCKETBASE_ADMIN_EMAIL; const ADMIN_PASSWORD = process.env.POCKETBASE_ADMIN_PASSWORD; if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { throw new Error( "Set POCKETBASE_ADMIN_EMAIL and POCKETBASE_ADMIN_PASSWORD in the environment." ); } const NEW_MIME_TYPES = [ "application/pdf", "image/png", "image/jpeg", "image/gif", "image/webp", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "text/plain", "application/octet-stream", ]; async function main() { const pb = new PocketBase(PB_URL); console.log(`Authenticating as ${ADMIN_EMAIL} on ${PB_URL}…`); await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); const list = await pb.collections.getList(1, 1, { filter: 'name = "quote_attachments"' }); if (list.items.length === 0) { throw new Error('Collection "quote_attachments" not found.'); } const col = list.items[0]; console.log(`Found quote_attachments (id=${col.id}).`); const fileField = col.fields.find((f) => f.name === "file"); if (!fileField) { throw new Error('No "file" field on quote_attachments.'); } // PB stores file-field options in two possible shapes depending on // how the collection was created: top-level `mimeTypes` (the API // read shape returned by GET /api/collections) or nested under // `options.mimeTypes` (the create/update write shape). We check // both so the diff is accurate. type FileField = { mimeTypes?: unknown; options?: { mimeTypes?: unknown } }; const ff = fileField as unknown as FileField; const currentMimes: string[] = Array.isArray(ff.mimeTypes) ? (ff.mimeTypes as string[]) : ((ff.options?.mimeTypes as string[] | undefined) ?? []); const missing = NEW_MIME_TYPES.filter((m) => !currentMimes.includes(m)); if (missing.length === 0) { console.log("Allowlist already up to date. Nothing to do."); return; } console.log(`Adding ${missing.length} mime type(s): ${missing.join(", ")}`); const newFields = col.fields.map((f) => { if (f.name !== "file") return f; // Write to BOTH shapes so the change persists regardless of which // shape PB's collections.update normalises into. PB ignores the // unknown one and stores the one it expects. const current = f as unknown as FileField; return { ...f, mimeTypes: NEW_MIME_TYPES, options: { ...(current.options || {}), mimeTypes: NEW_MIME_TYPES, }, }; }); await pb.collections.update(col.id, { fields: newFields }); console.log("✓ quote_attachments.file.mimeTypes updated."); } main().catch((err) => { console.error(err); process.exit(1); });