import Database from "better-sqlite3"; import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const DB_PATH = process.env.APOTHECARY_DB ?? join(__dirname, "..", "data.db"); export const db = new Database(DB_PATH); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); archiveLegacyIfPresent(); const schema = readFileSync(join(__dirname, "schema.sql"), "utf8"); db.exec(schema); // One-shot migration: the old schema put per-instance fields (weight, bin_id, // etc.) directly on `products`. The new schema splits products (catalog) from // inventory_items (instance). When we detect the old shape, rename the live // tables out of the way so the new CREATE TABLE IF NOT EXISTS statements can // build the empty new ones. Legacy tables stay queryable for reference. function archiveLegacyIfPresent(): void { const productCols = db .prepare(`PRAGMA table_info(products)`) .all() as { name: string }[]; const looksLikeOldSchema = productCols.length > 0 && productCols.some((c) => c.name === "weight"); if (!looksLikeOldSchema) return; const legacyExists = db .prepare( `SELECT name FROM sqlite_master WHERE type='table' AND name='products_legacy'`, ) .get(); if (legacyExists) return; db.exec(` ALTER TABLE products RENAME TO products_legacy; ALTER TABLE audits RENAME TO audits_legacy; ALTER TABLE strains RENAME TO strains_legacy; `); } const PAD: Record = { prd: 4, pdt: 4, inv: 4, str: 4, brd: 2, shp: 2, bin: 2, }; export function nextId(prefix: string, table: string): string { const row = db .prepare<[string], { id: string }>( `SELECT id FROM ${table} WHERE id LIKE ? ORDER BY id DESC LIMIT 1`, ) .get(`${prefix}-%`); const last = row ? Number(row.id.slice(prefix.length + 1)) : 0; const pad = PAD[prefix] ?? 2; const n = String(last + 1).padStart(pad, "0"); return `${prefix}-${n}`; } // Crockford base32 alphabet — drops I, L, O, U so labels are unambiguous when // read off a sticker. 32^6 ≈ 1B addresses, so collisions on any reasonable // inventory are vanishingly rare; we still loop just to be safe. const ASSET_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; export function generateAssetId(): string { for (let attempt = 0; attempt < 16; attempt++) { let id = ""; for (let i = 0; i < 6; i++) { id += ASSET_ALPHABET[Math.floor(Math.random() * ASSET_ALPHABET.length)]; } const taken = db .prepare<[string], { id: string }>( `SELECT id FROM inventory_items WHERE asset_id = ?`, ) .get(id); if (!taken) return id; } throw new Error("could not generate a unique asset id after 16 attempts"); } export function randomSku(): string { return "SKU-" + Math.random().toString(36).slice(2, 8).toUpperCase(); }