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.
60 lines
1.5 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|