// scripts/_create_vat_collection.js // One-shot: create the ysterbaard_SARS_VAT_VAT201 collection via the // PocketBase admin API. Safe to re-run: PB returns 400 on duplicate // collection name, which we treat as "already exists". const fs = require("node:fs"); const path = require("node:path"); // Load .env.local manually (no dotenv in node_modules) const ENV_PATH = path.join(__dirname, "..", ".env.local"); for (const line of fs.readFileSync(ENV_PATH, "utf-8").split(/\r?\n/)) { const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim(); } const PB_URL = process.env.PB_URL; const PB_TOKEN = process.env.PB_TOKEN; if (!PB_URL || !PB_TOKEN) { console.error("PB_URL / PB_TOKEN missing in .env.local"); process.exit(1); } const COLLECTION_NAME = "ysterbaard_SARS_VAT_VAT201"; const body = { name: COLLECTION_NAME, type: "base", schema: [ { name: "period", type: "text", required: true, options: { min: 6, max: 6, pattern: "^\\d{6}$" }, }, { name: "Standard Rate 1", type: "number", required: false, options: {}, }, { name: "Zero Rate 2", type: "number", required: false, options: {}, }, { name: "Exempt and non Supplies 3", type: "number", required: false, options: {}, }, { name: "Capital goods and services supplied to you 14", type: "number", required: false, options: {}, }, { name: "Other goods and services supplied to you 15", type: "number", required: false, options: {}, }, { name: "total_payable", type: "number", required: false, options: {} }, { name: "interest_d", type: "number", required: false, options: {} }, { name: "penalties_d", type: "number", required: false, options: {} }, { name: "status", type: "select", required: true, options: { maxSelect: 1, values: ["draft", "submitted", "paid"] }, }, { name: "date_submitted", type: "date", required: false, options: { min: "", max: "" } }, { name: "date_paid", type: "date", required: false, options: { min: "", max: "" } }, { name: "due_date", type: "date", required: false, options: { min: "", max: "" } }, { name: "payment_reference", type: "text", required: false, options: { min: 0, max: 100, pattern: "" }, }, { // Hidden upsert key — mirrors EMP201 name: "company_period_key", type: "text", required: false, options: { min: 0, max: 0, pattern: "" }, }, { name: "company", type: "relation", required: true, options: { collectionId: "pbc_3748838150", // ysterbaard_companies cascadeDelete: false, minSelect: 1, maxSelect: 1, }, }, { name: "Return_Filed", type: "bool", required: false, options: {} }, { name: "last_updated", type: "date", required: false, options: { min: "", max: "" } }, { name: "notes", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } }, // ── Contact fields (added 2026-09-13) ────────────────────── { name: "First Name", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } }, { name: "Surname", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } }, { name: "Capacity", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } }, { name: "Bus Tel no", type: "number", required: false, options: {} }, { name: "Cell no", type: "number", required: false, options: {} }, { name: "Contact Email", type: "email", required: false, options: { exceptDomains: [], onlyDomains: [] } }, // ── End contact fields ───────────────────────────────────── { name: "is_active", type: "bool", required: false, options: {} }, { name: "documents", type: "file", required: false, options: { maxSelect: 10, maxSize: 10485760, mimeTypes: ["application/pdf", "image/png", "image/jpeg"], thumbs: null, protected: false, }, }, ], indexes: [ "CREATE UNIQUE INDEX idx_vat201_company_period ON ysterbaard_SARS_VAT_VAT201 (company_period_key)", "CREATE INDEX idx_vat201_period ON ysterbaard_SARS_VAT_VAT201 (period)", "CREATE INDEX idx_vat201_status ON ysterbaard_SARS_VAT_VAT201 (status)", ], listRule: null, viewRule: null, createRule: null, updateRule: null, deleteRule: null, }; (async () => { console.log(`Target: ${body.name} (${body.schema.length} user-defined fields + ${body.indexes.length} indexes)`); // Step 1: POST the collection with schema only. PB will create the // collection and its columns. Indexes are added separately because // PB validates indexes against existing columns; if we POST them // in the same call the columns don't exist yet and the whole // transaction rolls back. const indexes = body.indexes; const createBody = { ...body, indexes: [] }; console.log(`\n[1/2] POST /api/collections (schema only)`); const res = await fetch(`${PB_URL}/api/collections`, { method: "POST", headers: { Authorization: PB_TOKEN, "Content-Type": "application/json", }, body: JSON.stringify(createBody), }); const text = await res.text(); console.log(`HTTP ${res.status}`); if (!res.ok) { console.error(`Failed: ${text.slice(0, 500)}`); if (res.status === 400 && /already exists/i.test(text)) { console.error("→ Collection already exists. Skipping create; will patch indexes."); } else { process.exit(1); } } const created = res.ok ? JSON.parse(text) : null; const collectionId = created?.id; if (created) { console.log(`Created collection id: ${collectionId}`); console.log(`Fields persisted: ${(created.schema || []).length}`); } else { // Look up the existing collection id by name const list = await fetch(`${PB_URL}/api/collections?perPage=200`, { headers: { Authorization: PB_TOKEN }, }); const all = (await list.json()).items || []; const existing = all.find((c) => c.name === body.name); if (!existing) { console.error("Could not find existing collection. Aborting."); process.exit(1); } console.log(`Found existing id: ${existing.id}`); } const targetId = collectionId || (await (async () => { const list = await fetch(`${PB_URL}/api/collections?perPage=200`, { headers: { Authorization: PB_TOKEN } }); return ((await list.json()).items || []).find((c) => c.name === body.name)?.id; })()); if (!targetId) { console.error("No collection id resolved. Aborting."); process.exit(1); } // Step 2: PATCH the collection to add the indexes. By now the // columns exist, so the CREATE INDEX statements succeed. console.log(`\n[2/2] PATCH /api/collections/${targetId} (add indexes)`); const patch = await fetch(`${PB_URL}/api/collections/${targetId}`, { method: "PATCH", headers: { Authorization: PB_TOKEN, "Content-Type": "application/json", }, body: JSON.stringify({ indexes }), }); const patchText = await patch.text(); console.log(`HTTP ${patch.status}`); if (!patch.ok) { console.error(`Failed: ${patchText.slice(0, 500)}`); process.exit(1); } const final = JSON.parse(patchText); console.log(`Final indexes: ${(final.indexes || []).length}`); for (const ix of final.indexes || []) console.log(` - ${ix}`); // Step 3: verify field count console.log(`\nVerify: GET /api/collections/${targetId}`); const verify = await fetch(`${PB_URL}/api/collections/${targetId}`, { headers: { Authorization: PB_TOKEN }, }); const verifyJson = await verify.json(); console.log(` fields: ${(verifyJson.schema || []).length}`); console.log(` indexes: ${(verifyJson.indexes || []).length}`); console.log(` listRule: ${JSON.stringify(verifyJson.listRule)}`); console.log(` name: ${verifyJson.name}`); console.log(`\nDone.`); process.exit(0); })().catch((err) => { console.error("Unexpected error:", err); process.exit(1); });