Add PocketBase collections, APIs & migration docs
Introduce PocketBase workflow support, migration tooling, server APIs and documentation. Adds a set of PocketBase collection JSONs (order_items, order_items_v2, collection_pool, delivery_notes, delivery_note_items, repairs) and a large import manifest for workflow collections; includes docs describing MCP/API layer, migration audit, order-item schema and rollout plans. Adds server-side agent/MCP endpoints and services (api/mcp, api/v1/orders, collection-pool, order-items status, health) plus helper modules (pocketbase-admin, orders-service, order-item-status, agent-auth) and a migration script to create order_items_v2/collection_pool. UI: new pages for collection-pool, delivery-notes, repairs and several updates to orders/quotes pages, sidebar, tooltip and pb helpers/types. Also updates package.json (migration script and related changes) and global styles. Required env vars and suggested migration command are documented (e.g. NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, ORDER_ITEMS_COLLECTION; run npm run pb:migrate:orders-v2).
2026-05-25 16:39:33 +00:00
|
|
|
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 adminEmail = ADMIN_EMAIL;
|
|
|
|
|
const adminPassword = ADMIN_PASSWORD;
|
|
|
|
|
|
|
|
|
|
const ROOT = path.resolve(process.cwd(), "docs", "pocketbase");
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
const pb = new PocketBase(PB_URL);
|
|
|
|
|
await pb.collection("_superusers").authWithPassword(adminEmail, adminPassword);
|
|
|
|
|
|
|
|
|
|
const collections = await pb.collections.getFullList();
|
|
|
|
|
const byName = new Map(collections.map((c) => [c.name, c.id]));
|
|
|
|
|
|
|
|
|
|
const replacers: Record<string, string> = {
|
|
|
|
|
REPLACE_ORDERS_COLLECTION_ID: byName.get("orders") || "",
|
|
|
|
|
REPLACE_QUOTE_ITEMS_COLLECTION_ID: byName.get("quote_items") || "",
|
|
|
|
|
REPLACE_PURCHASE_ORDERS_COLLECTION_ID: byName.get("purchase_orders") || "",
|
|
|
|
|
REPLACE_SUPPLIERS_COLLECTION_ID: byName.get("suppliers") || "",
|
|
|
|
|
REPLACE_CUSTOMERS_COLLECTION_ID: byName.get("customers") || "",
|
|
|
|
|
REPLACE_COMPANIES_COLLECTION_ID: byName.get("companies") || "",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const key of Object.keys(replacers)) {
|
|
|
|
|
if (!replacers[key]) {
|
|
|
|
|
throw new Error(`Missing required collection for token ${key}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const orderItemsDef = JSON.parse(fs.readFileSync(path.join(ROOT, "order_items.collection.json"), "utf8"));
|
|
|
|
|
const existingOrderItems = collections.find((c) => c.name === "order_items");
|
|
|
|
|
if (existingOrderItems) {
|
|
|
|
|
console.log("order_items already exists. Delete it first if you want a clean replace.");
|
|
|
|
|
} else {
|
|
|
|
|
const normalized = JSON.parse(JSON.stringify(orderItemsDef)
|
|
|
|
|
.replaceAll("REPLACE_ORDERS_COLLECTION_ID", replacers.REPLACE_ORDERS_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_QUOTE_ITEMS_COLLECTION_ID", replacers.REPLACE_QUOTE_ITEMS_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_PURCHASE_ORDERS_COLLECTION_ID", replacers.REPLACE_PURCHASE_ORDERS_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_SUPPLIERS_COLLECTION_ID", replacers.REPLACE_SUPPLIERS_COLLECTION_ID));
|
|
|
|
|
|
|
|
|
|
const created = await pb.collections.create(normalized);
|
|
|
|
|
console.log(`Created ${created.name} (${created.id})`);
|
|
|
|
|
byName.set(created.name, created.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const collectionPoolDef = JSON.parse(fs.readFileSync(path.join(ROOT, "collection_pool.collection.json"), "utf8"));
|
|
|
|
|
const existingPool = (await pb.collections.getFullList()).find((c) => c.name === "collection_pool");
|
|
|
|
|
if (existingPool) {
|
|
|
|
|
console.log("collection_pool already exists, skipping create.");
|
|
|
|
|
} else {
|
|
|
|
|
const orderItemsId = byName.get("order_items");
|
|
|
|
|
if (!orderItemsId) {
|
|
|
|
|
throw new Error("order_items was not found/created; cannot create collection_pool.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedPool = JSON.parse(JSON.stringify(collectionPoolDef)
|
|
|
|
|
.replaceAll("REPLACE_ORDERS_COLLECTION_ID", replacers.REPLACE_ORDERS_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_ORDER_ITEMS_COLLECTION_ID", orderItemsId)
|
|
|
|
|
.replaceAll("REPLACE_SUPPLIERS_COLLECTION_ID", replacers.REPLACE_SUPPLIERS_COLLECTION_ID));
|
|
|
|
|
|
|
|
|
|
const createdPool = await pb.collections.create(normalizedPool);
|
|
|
|
|
console.log(`Created ${createdPool.name} (${createdPool.id})`);
|
|
|
|
|
byName.set(createdPool.name, createdPool.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deliveryNotesDef = JSON.parse(fs.readFileSync(path.join(ROOT, "delivery_notes.collection.json"), "utf8"));
|
|
|
|
|
const existingDeliveryNotes = (await pb.collections.getFullList()).find((c) => c.name === "delivery_notes");
|
|
|
|
|
if (existingDeliveryNotes) {
|
|
|
|
|
console.log("delivery_notes already exists, skipping create.");
|
|
|
|
|
byName.set(existingDeliveryNotes.name, existingDeliveryNotes.id);
|
|
|
|
|
} else {
|
|
|
|
|
const normalizedDeliveryNotes = JSON.parse(JSON.stringify(deliveryNotesDef)
|
|
|
|
|
.replaceAll("REPLACE_ORDERS_COLLECTION_ID", replacers.REPLACE_ORDERS_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_COMPANIES_COLLECTION_ID", replacers.REPLACE_COMPANIES_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_CUSTOMERS_COLLECTION_ID", replacers.REPLACE_CUSTOMERS_COLLECTION_ID));
|
|
|
|
|
|
|
|
|
|
const createdDeliveryNotes = await pb.collections.create(normalizedDeliveryNotes);
|
|
|
|
|
console.log(`Created ${createdDeliveryNotes.name} (${createdDeliveryNotes.id})`);
|
|
|
|
|
byName.set(createdDeliveryNotes.name, createdDeliveryNotes.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deliveryNoteItemsDef = JSON.parse(fs.readFileSync(path.join(ROOT, "delivery_note_items.collection.json"), "utf8"));
|
|
|
|
|
const existingDeliveryNoteItems = (await pb.collections.getFullList()).find((c) => c.name === "delivery_note_items");
|
|
|
|
|
if (existingDeliveryNoteItems) {
|
|
|
|
|
console.log("delivery_note_items already exists, skipping create.");
|
|
|
|
|
} else {
|
|
|
|
|
const deliveryNotesId = byName.get("delivery_notes");
|
|
|
|
|
const orderItemsId = byName.get("order_items");
|
|
|
|
|
if (!deliveryNotesId || !orderItemsId) {
|
|
|
|
|
throw new Error("delivery_notes and order_items are required before creating delivery_note_items.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedDeliveryNoteItems = JSON.parse(JSON.stringify(deliveryNoteItemsDef)
|
|
|
|
|
.replaceAll("REPLACE_DELIVERY_NOTES_COLLECTION_ID", deliveryNotesId)
|
|
|
|
|
.replaceAll("REPLACE_ORDER_ITEMS_COLLECTION_ID", orderItemsId));
|
|
|
|
|
|
|
|
|
|
const createdDeliveryNoteItems = await pb.collections.create(normalizedDeliveryNoteItems);
|
|
|
|
|
console.log(`Created ${createdDeliveryNoteItems.name} (${createdDeliveryNoteItems.id})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const repairsDef = JSON.parse(fs.readFileSync(path.join(ROOT, "repairs.collection.json"), "utf8"));
|
|
|
|
|
const existingRepairs = (await pb.collections.getFullList()).find((c) => c.name === "repairs");
|
|
|
|
|
if (existingRepairs) {
|
|
|
|
|
console.log("repairs already exists, skipping create.");
|
|
|
|
|
} else {
|
|
|
|
|
const normalizedRepairs = JSON.parse(JSON.stringify(repairsDef)
|
|
|
|
|
.replaceAll("REPLACE_CUSTOMERS_COLLECTION_ID", replacers.REPLACE_CUSTOMERS_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_COMPANIES_COLLECTION_ID", replacers.REPLACE_COMPANIES_COLLECTION_ID)
|
|
|
|
|
.replaceAll("REPLACE_ORDERS_COLLECTION_ID", replacers.REPLACE_ORDERS_COLLECTION_ID));
|
|
|
|
|
|
|
|
|
|
const createdRepairs = await pb.collections.create(normalizedRepairs);
|
|
|
|
|
console.log(`Created ${createdRepairs.name} (${createdRepairs.id})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("Migration complete.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch((err) => {
|
|
|
|
|
console.error(err);
|
|
|
|
|
process.exit(1);
|
2026-06-11 18:30:02 +00:00
|
|
|
});
|