import Database from 'better-sqlite3'; import { Customer, CustomerRepo } from '../auth'; export class SqliteCustomerRepo implements CustomerRepo { constructor(private db: Database.Database, seed: Customer[] = []) { this.db.exec(` CREATE TABLE IF NOT EXISTS customers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, tier_id TEXT NOT NULL, api_key TEXT NOT NULL UNIQUE, stripe_customer_id TEXT, multiplier_override REAL ) `); // Idempotent migrations for pre-existing databases. this.ensureColumn('billing_type', `billing_type TEXT NOT NULL DEFAULT 'stripe'`); this.ensureColumn('email', 'email TEXT'); this.ensureColumn('password_hash', 'password_hash TEXT'); this.ensureColumn('totp_secret', 'totp_secret TEXT'); this.ensureColumn('totp_enabled', 'totp_enabled INTEGER NOT NULL DEFAULT 0'); this.ensureColumn('balance_cents', 'balance_cents INTEGER NOT NULL DEFAULT 0'); this.ensureColumn('email_invoicing', 'email_invoicing INTEGER NOT NULL DEFAULT 0'); const { n } = this.db.prepare('SELECT COUNT(*) AS n FROM customers').get() as { n: number }; if (n === 0) { for (const c of seed) this.save(c); } } private ensureColumn(name: string, ddl: string): void { const cols = this.db.prepare('PRAGMA table_info(customers)').all() as { name: string }[]; if (!cols.some((c) => c.name === name)) { this.db.exec(`ALTER TABLE customers ADD COLUMN ${ddl}`); } } findByApiKey(apiKey: string): Customer | undefined { const r = this.db.prepare('SELECT * FROM customers WHERE api_key = ?').get(apiKey) as | Record | undefined; return r ? toCustomer(r) : undefined; } findByEmail(email: string): Customer | undefined { const r = this.db .prepare('SELECT * FROM customers WHERE lower(email) = lower(?)') .get(email) as Record | undefined; return r ? toCustomer(r) : undefined; } list(): Customer[] { const rows = this.db.prepare('SELECT * FROM customers ORDER BY id').all() as Record< string, unknown >[]; return rows.map(toCustomer); } save(customer: Customer): void { this.db .prepare( `INSERT INTO customers (id, name, tier_id, api_key, stripe_customer_id, multiplier_override, billing_type, email, password_hash, totp_secret, totp_enabled, balance_cents, email_invoicing) VALUES (@id, @name, @tierId, @apiKey, @stripeCustomerId, @multiplierOverride, @billingType, @email, @passwordHash, @totpSecret, @totpEnabled, @balanceCents, @emailInvoicing) ON CONFLICT(id) DO UPDATE SET name = @name, tier_id = @tierId, api_key = @apiKey, stripe_customer_id = @stripeCustomerId, multiplier_override = @multiplierOverride, billing_type = @billingType, email = @email, password_hash = @passwordHash, totp_secret = @totpSecret, totp_enabled = @totpEnabled, balance_cents = @balanceCents, email_invoicing = @emailInvoicing`, ) .run({ id: customer.id, name: customer.name, tierId: customer.tierId, apiKey: customer.apiKey, stripeCustomerId: customer.stripeCustomerId ?? null, multiplierOverride: customer.multiplierOverride ?? null, billingType: customer.billingType ?? 'stripe', email: customer.email ?? null, passwordHash: customer.passwordHash ?? null, totpSecret: customer.totpSecret ?? null, totpEnabled: customer.totpEnabled ? 1 : 0, balanceCents: customer.balanceCents ?? 0, emailInvoicing: customer.emailInvoicing ? 1 : 0, }); } } function toCustomer(r: Record): Customer { return { id: r.id as string, name: r.name as string, tierId: r.tier_id as string, apiKey: r.api_key as string, stripeCustomerId: (r.stripe_customer_id as string | null) ?? undefined, multiplierOverride: (r.multiplier_override as number | null) ?? undefined, billingType: ((r.billing_type as string | null) ?? 'stripe') as Customer['billingType'], email: (r.email as string | null) ?? undefined, passwordHash: (r.password_hash as string | null) ?? undefined, totpSecret: (r.totp_secret as string | null) ?? undefined, totpEnabled: ((r.totp_enabled as number | null) ?? 0) === 1, balanceCents: (r.balance_cents as number | null) ?? 0, emailInvoicing: ((r.email_invoicing as number | null) ?? 0) === 1, }; }