app/: production port — Postgres + Prisma + OTM-account auth (v0.2.0)
Prototype stays untouched (Bonna's laptop daily driver until cutover). app/ is the deployable bms-backbone: same routes/frontend, Prisma models replacing node:sqlite, session-cookie login backed by a User table seeded via the platform SEED_OWNER_* convention (bcryptjs), /api/health with APP_VERSION, photos/files under DATA_DIR (/app/data uploads mount), Dockerfile + entrypoint (prisma db push + owner seed + serve), and a one-shot sqlite→postgres migration script for Bonna's existing data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
65
app/scripts/migrate-sqlite.mjs
Normal file
65
app/scripts/migrate-sqlite.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
// One-shot: copy Bonna's laptop prototype data (SQLite) into the hosted
|
||||
// Postgres. Run from app/ with DATABASE_URL set:
|
||||
// node scripts/migrate-sqlite.mjs /path/to/prototype/data/studio.db
|
||||
// Idempotent-ish: skips any table that already has rows in Postgres.
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const src = process.argv[2];
|
||||
if (!src) { console.error("usage: node scripts/migrate-sqlite.mjs <studio.db>"); process.exit(1); }
|
||||
const lite = new DatabaseSync(src);
|
||||
const db = new PrismaClient();
|
||||
const rows = (sql) => { try { return lite.prepare(sql).all(); } catch { return []; } };
|
||||
const d = (s) => (s ? new Date(s.replace(" ", "T")) : new Date());
|
||||
|
||||
async function copy(name, model, sqliteSql, map) {
|
||||
if ((await model.count()) > 0) return console.log(`${name}: postgres has rows — skipped`);
|
||||
const data = rows(sqliteSql);
|
||||
for (const r of data) await model.create({ data: map(r) });
|
||||
console.log(`${name}: ${data.length} migrated`);
|
||||
}
|
||||
|
||||
// order matters (children reference parents; ids preserved by inserting in id order)
|
||||
await copy("suppliers", db.supplier, "SELECT * FROM suppliers ORDER BY id", (r) => ({
|
||||
id: r.id, name: r.name, kind: r.kind, distance: r.distance, url: r.url, notes: r.notes,
|
||||
searchUrl: r.search_url || "", created: d(r.created) }));
|
||||
await copy("price_entries", db.priceEntry, "SELECT * FROM price_entries ORDER BY id", (r) => ({
|
||||
id: r.id, item: r.item, supplierId: r.supplier_id, price: r.price, unit: r.unit,
|
||||
shipping: r.shipping, note: r.note, created: d(r.created) }));
|
||||
await copy("glazes", db.glaze, "SELECT * FROM glazes ORDER BY id", (r) => ({
|
||||
id: r.id, name: r.name, kind: r.kind, cone: r.cone, atmosphere: r.atmosphere, surface: r.surface,
|
||||
source: r.source, sg: r.sg, swatch: r.swatch, created: d(r.created) }));
|
||||
await copy("glaze_materials", db.glazeMaterial, "SELECT * FROM glaze_materials ORDER BY id", (r) => ({
|
||||
id: r.id, glazeId: r.glaze_id, name: r.name, pct: r.pct, addition: !!r.addition }));
|
||||
await copy("glaze_journal", db.glazeJournal, "SELECT * FROM glaze_journal ORDER BY id", (r) => ({
|
||||
id: r.id, glazeId: r.glaze_id, text: r.text, created: d(r.created) }));
|
||||
await copy("firings", db.firing, "SELECT * FROM firings ORDER BY id", (r) => ({
|
||||
id: r.id, type: r.type, cone: r.cone, schedule: r.schedule, status: r.status,
|
||||
resultNotes: r.result_notes, started: d(r.started), unloaded: r.unloaded ? d(r.unloaded) : null }));
|
||||
await copy("firing_items", db.firingItem, "SELECT * FROM firing_items ORDER BY id", (r) => ({
|
||||
id: r.id, firingId: r.firing_id, piece: r.piece, clay: r.clay, glaze: r.glaze, rating: r.rating, note: r.note }));
|
||||
await copy("products", db.product, "SELECT * FROM products ORDER BY id", (r) => ({
|
||||
id: r.id, name: r.name, kind: r.kind, category: r.category, price: r.price, cost: r.cost,
|
||||
qty: r.qty, status: r.status, notes: r.notes || "", created: d(r.created) }));
|
||||
await copy("sales", db.sale, "SELECT * FROM sales ORDER BY id", (r) => ({
|
||||
id: r.id, productId: r.product_id, qty: r.qty, price: r.price, cost: r.cost, created: d(r.created) }));
|
||||
await copy("todos", db.todo, "SELECT * FROM todos ORDER BY id", (r) => ({
|
||||
id: r.id, text: r.text, done: !!r.done, parentId: r.parent_id, created: d(r.created) }));
|
||||
await copy("shopping", db.shopping, "SELECT * FROM shopping ORDER BY id", (r) => ({
|
||||
id: r.id, text: r.text, done: !!r.done, created: d(r.created) }));
|
||||
await copy("notes", db.note, "SELECT * FROM notes ORDER BY id", (r) => ({
|
||||
id: r.id, text: r.text, tags: r.tags, created: d(r.created) }));
|
||||
await copy("photos", db.photo, "SELECT * FROM photos ORDER BY id", (r) => ({
|
||||
id: r.id, entity: r.entity, entityId: r.entity_id, filename: r.filename, created: d(r.created) }));
|
||||
await copy("files", db.fileAsset, "SELECT * FROM files ORDER BY id", (r) => ({
|
||||
id: r.id, entity: r.entity, entityId: r.entity_id, stored: r.stored, name: r.name,
|
||||
tag: r.tag, size: r.size, created: d(r.created) }));
|
||||
|
||||
// bump Postgres sequences past the migrated ids
|
||||
for (const t of ["User", "Note", "Todo", "Shopping", "Glaze", "GlazeMaterial", "GlazeJournal",
|
||||
"Firing", "FiringItem", "Photo", "FileAsset", "Product", "Sale", "Supplier", "PriceEntry"]) {
|
||||
await db.$executeRawUnsafe(
|
||||
`SELECT setval(pg_get_serial_sequence('"${t}"','id'), COALESCE((SELECT MAX(id) FROM "${t}"), 0) + 1, false)`);
|
||||
}
|
||||
console.log("sequences bumped — done 🌙 (photos/files binaries: copy prototype/data/{photos,files} into the container's /app/data)");
|
||||
await db.$disconnect();
|
||||
Reference in New Issue
Block a user