80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
|
|
/* eslint-disable */
|
||
|
|
/**
|
||
|
|
* Patch: set maxSelect=10 on the new `outbound_attachments` and
|
||
|
|
* `internal_attachments` file fields on `quotes`. The initial
|
||
|
|
* migration nested the options under `options: {...}` which PB stored
|
||
|
|
* as maxSelect=0 (= "unlimited" for files). File fields need the
|
||
|
|
* options flat at the top level of the field object, like the
|
||
|
|
* existing `quote_attachments.file` and friends.
|
||
|
|
*
|
||
|
|
* Idempotent — runs to no-op once maxSelect is 10.
|
||
|
|
*
|
||
|
|
* Run with:
|
||
|
|
* npx tsx scripts/migrations/2026-09-09-fix-quote-attachment-fields-maxselect.ts
|
||
|
|
*/
|
||
|
|
import PocketBase from "pocketbase";
|
||
|
|
import { readFileSync } from "node:fs";
|
||
|
|
import { resolve } from "node:path";
|
||
|
|
|
||
|
|
function loadEnvFile(name: string) {
|
||
|
|
try {
|
||
|
|
const raw = readFileSync(resolve(process.cwd(), name), "utf8");
|
||
|
|
for (const line of raw.split("\n")) {
|
||
|
|
const trimmed = line.trim();
|
||
|
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||
|
|
const m = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(.+)$/i);
|
||
|
|
if (m && !process.env[m[1]]) {
|
||
|
|
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
/* not present */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
loadEnvFile(".env");
|
||
|
|
loadEnvFile(".env.local");
|
||
|
|
|
||
|
|
const FIELDS = ["outbound_attachments", "internal_attachments"] as const;
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const url = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
||
|
|
const email = process.env.PB_ADMIN_EMAIL;
|
||
|
|
const password = process.env.PB_ADMIN_PASSWORD;
|
||
|
|
if (!url || !email || !password) {
|
||
|
|
throw new Error(
|
||
|
|
"Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD.",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const pb = new PocketBase(url);
|
||
|
|
await pb.collection("_superusers").authWithPassword(email, password);
|
||
|
|
console.log(`[quote-attachment-fields-maxselect] Authenticated against ${url}`);
|
||
|
|
|
||
|
|
const quotes = await pb.collections.getOne("quotes");
|
||
|
|
const fields = (quotes.fields as Array<Record<string, unknown> & { name: string }>).map(
|
||
|
|
(f) =>
|
||
|
|
FIELDS.includes((f.name as (typeof FIELDS)[number]))
|
||
|
|
? { ...f, maxSelect: 10, mimeTypes: [] }
|
||
|
|
: f,
|
||
|
|
);
|
||
|
|
|
||
|
|
// PB validates that the update payload includes collection rules + the same
|
||
|
|
// shape it returned. Send the unchanged rules/options/indexes through.
|
||
|
|
await pb.collections.update(quotes.id, {
|
||
|
|
fields,
|
||
|
|
indexes: quotes.indexes,
|
||
|
|
listRule: quotes.listRule,
|
||
|
|
viewRule: quotes.viewRule,
|
||
|
|
createRule: quotes.createRule,
|
||
|
|
updateRule: quotes.updateRule,
|
||
|
|
deleteRule: quotes.deleteRule,
|
||
|
|
});
|
||
|
|
console.log(
|
||
|
|
`[quote-attachment-fields-maxselect] Patched ${FIELDS.join(", ")} to maxSelect=10`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((err) => {
|
||
|
|
console.error("[quote-attachment-fields-maxselect] failed:", err);
|
||
|
|
process.exit(1);
|
||
|
|
});
|