import fs from "node:fs"; import path from "node:path"; import PocketBase from "pocketbase"; const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090"; const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL; const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD; if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { throw new Error("PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD are required."); } const ROOT = path.resolve(process.cwd(), "docs", "pocketbase"); /** * Idempotent migration that creates the quote-email-intake collections: * - quote_attachments (relation to quotes; holds email PDFs/images) * - quote_replies (relation to quotes; holds email reply threads) * - quote_counters (atomic year/month sequence for quote numbers) * * Also adds the `email_message_id` field to the existing `quotes` collection * (for inbox-dedup). Re-runs are safe: every step checks for existence first. */ async function main() { const pb = new PocketBase(PB_URL); await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); const collections = await pb.collections.getFullList(); const byName = new Map(collections.map((c) => [c.name, c])); const quotes = byName.get("quotes"); if (!quotes) { throw new Error("Missing required collection: quotes. Run `npm run setup` first."); } const quotesId = quotes.id; // ── 1. Add email_message_id to existing quotes collection (idempotent) ── const quotesHasMessageId = quotes.fields.some((f) => f.name === "email_message_id"); if (quotesHasMessageId) { console.log("quotes.email_message_id already exists, skipping field add."); } else { await pb.collections.update(quotesId, { fields: [ ...quotes.fields, { name: "email_message_id", type: "text", required: false, max: 255 }, ], }); // Add a unique-ish index to make Message-ID lookups O(log n) and to give us // a strong signal even if the unique constraint is later relaxed. try { await pb.collections.update(quotesId, { indexes: [ ...(quotes.indexes || []), "CREATE INDEX idx_quotes_email_message_id ON quotes (email_message_id)", ], }); console.log("Added index idx_quotes_email_message_id on quotes."); } catch (err) { console.warn("Could not add email_message_id index (may already exist):", err); } console.log("Added quotes.email_message_id field."); } // ── 2. quote_attachments ── const existingAttachments = byName.get("quote_attachments"); if (existingAttachments) { console.log("quote_attachments already exists, skipping create."); } else { const def = JSON.parse(fs.readFileSync(path.join(ROOT, "quote_attachments.collection.json"), "utf8")); const normalized = JSON.parse(JSON.stringify(def).replaceAll("REPLACE_QUOTES_COLLECTION_ID", quotesId)); const created = await pb.collections.create(normalized); console.log(`Created ${created.name} (${created.id})`); } // ── 3. quote_replies ── const existingReplies = byName.get("quote_replies"); if (existingReplies) { console.log("quote_replies already exists, skipping create."); } else { const def = JSON.parse(fs.readFileSync(path.join(ROOT, "quote_replies.collection.json"), "utf8")); const normalized = JSON.parse(JSON.stringify(def).replaceAll("REPLACE_QUOTES_COLLECTION_ID", quotesId)); const created = await pb.collections.create(normalized); console.log(`Created ${created.name} (${created.id})`); } // ── 4. quote_counters ── const existingCounters = byName.get("quote_counters"); if (existingCounters) { console.log("quote_counters already exists, skipping create."); } else { const def = JSON.parse(fs.readFileSync(path.join(ROOT, "quote_counters.collection.json"), "utf8")); const created = await pb.collections.create(def); console.log(`Created ${created.name} (${created.id})`); } console.log("Quote email intake migration complete."); } main().catch((err) => { console.error(err); process.exit(1); });