WFDASH-WEBAPP/scripts/migrations/broaden-quote-attachments-mime-types.ts

96 lines
3.4 KiB
TypeScript
Raw Normal View History

/**
* 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);
});