Files
snowspeeder 59645e59b5 Add green-bean inventory and cupping features
Ports inventory management and SCA-style cupping scoring from
hope_roaster: lot tracking with audit-logged consumption, cupping
sessions with server-authoritative scoring and a live radar chart,
and links from the planner (draw-from-lot, open-cupping-session).

Also fixes the Blend/Single-origin toggle layout, replaces tooltips
with an in-context "why" teaching layer, and stages the planner UI
into pre-roast vs. post-roast phases.
2026-07-30 14:47:15 -04:00

60 lines
1.5 KiB
JavaScript

import pg from "pg";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
export function createDb(connectionString = process.env.DATABASE_URL) {
if (!connectionString) throw new Error("DATABASE_URL is required");
const pool = new pg.Pool({
connectionString,
max: 10,
ssl:
process.env.DATABASE_SSL === "true"
? { rejectUnauthorized: true }
: undefined,
});
return {
query: (...args) => pool.query(...args),
connect: () => pool.connect(),
close: () => pool.end(),
};
}
/** Apply each versioned SQL file once; failed migrations are not recorded. */
export async function migrate(db) {
await db.query(
"CREATE TABLE IF NOT EXISTS schema_migrations (filename text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())",
);
const directory = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"..",
"db",
"migrations",
);
const files = (await fs.readdir(directory))
.filter((file) => file.endsWith(".sql"))
.sort();
for (const filename of files) {
if (
(
await db.query("SELECT 1 FROM schema_migrations WHERE filename=$1", [
filename,
])
).rowCount
)
continue;
const sql = await fs.readFile(path.join(directory, filename), "utf8");
await db.query("BEGIN");
try {
await db.query(sql);
await db.query("INSERT INTO schema_migrations(filename) VALUES($1)", [
filename,
]);
await db.query("COMMIT");
} catch (error) {
await db.query("ROLLBACK");
throw error;
}
}
}