import { createClient, Client } from "@libsql/client"; import dotenv from "dotenv"; import fs from "fs"; import path from "path"; dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); // ============================================================================= // Exported utility functions for testing // ============================================================================= /** Get the full migration SQL string */ export function getMigrationSQL(): string { return ` CREATE TABLE IF NOT EXISTS users ( id integer PRIMARY KEY AUTOINCREMENT, username text NOT NULL UNIQUE, password_hash text, avatar_url text, auth_provider text NOT NULL DEFAULT 'local', oidc_subject text, is_active integer NOT NULL DEFAULT 1, last_login_at integer, created_at integer NOT NULL DEFAULT (strftime('%s','now')), updated_at integer NOT NULL DEFAULT (strftime('%s','now')) ); CREATE TABLE IF NOT EXISTS medications ( id integer PRIMARY KEY AUTOINCREMENT, user_id integer NOT NULL, name text NOT NULL, generic_name text, taken_by_json text NOT NULL DEFAULT '[]', pack_count integer NOT NULL DEFAULT 1, blisters_per_pack integer NOT NULL DEFAULT 1, pills_per_blister integer NOT NULL DEFAULT 1, loose_tablets integer NOT NULL DEFAULT 0, pill_weight_mg integer, usage_json text NOT NULL DEFAULT '[]', every_json text NOT NULL DEFAULT '[]', start_json text NOT NULL DEFAULT '[]', image_url text, expiry_date text, notes text, intake_reminders_enabled integer NOT NULL DEFAULT 0, updated_at integer NOT NULL DEFAULT (strftime('%s','now')), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_settings ( id integer PRIMARY KEY AUTOINCREMENT, user_id integer NOT NULL UNIQUE, email_enabled integer NOT NULL DEFAULT 0, notification_email text, email_stock_reminders integer NOT NULL DEFAULT 1, email_intake_reminders integer NOT NULL DEFAULT 1, shoutrrr_enabled integer NOT NULL DEFAULT 0, shoutrrr_url text, shoutrrr_stock_reminders integer NOT NULL DEFAULT 1, shoutrrr_intake_reminders integer NOT NULL DEFAULT 1, reminder_days_before integer NOT NULL DEFAULT 7, repeat_daily_reminders integer NOT NULL DEFAULT 0, low_stock_days integer NOT NULL DEFAULT 30, normal_stock_days integer NOT NULL DEFAULT 90, high_stock_days integer NOT NULL DEFAULT 180, language text NOT NULL DEFAULT 'en', stock_calculation_mode text NOT NULL DEFAULT 'automatic', last_auto_email_sent text, last_notification_type text, last_notification_channel text, updated_at integer NOT NULL DEFAULT (strftime('%s','now')), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS refresh_tokens ( id integer PRIMARY KEY AUTOINCREMENT, user_id integer NOT NULL, token_id text NOT NULL UNIQUE, expires_at integer NOT NULL, rotated_at integer, revoked integer NOT NULL DEFAULT 0, created_at integer NOT NULL DEFAULT (strftime('%s','now')), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS share_tokens ( id integer PRIMARY KEY AUTOINCREMENT, user_id integer NOT NULL, token text NOT NULL UNIQUE, taken_by text NOT NULL, schedule_days integer NOT NULL DEFAULT 30, created_at integer NOT NULL DEFAULT (strftime('%s','now')), expires_at integer, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS dose_tracking ( id integer PRIMARY KEY AUTOINCREMENT, user_id integer NOT NULL, dose_id text NOT NULL, taken_at integer NOT NULL DEFAULT (strftime('%s','now')), marked_by text, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); `; } /** Split SQL string into individual statements */ export function splitSQLStatements(sql: string): string[] { return sql.split(';').filter(s => s.trim().length > 0); } /** Execute migration statements on a client */ export async function executeMigration(client: Client): Promise<{ success: boolean; executed: number; errors: string[] }> { const sql = getMigrationSQL(); const statements = splitSQLStatements(sql); const errors: string[] = []; let executed = 0; for (const stmt of statements) { try { await client.execute(stmt); executed++; } catch (err: any) { errors.push(err.message); } } return { success: errors.length === 0, executed, errors }; } /** Get a preview of statement (first N characters) */ export function getStatementPreview(stmt: string, maxLength: number = 50): string { const trimmed = stmt.trim(); if (trimmed.length <= maxLength) { return trimmed; } return trimmed.substring(0, maxLength) + "..."; } // ============================================================================= // CLI execution (only runs when called directly) // ============================================================================= const url = "file:./data/medassist-ng.db"; async function main() { console.log("Starting database setup..."); console.log("Database URL:", url); const client = createClient({ url }); const sql = getMigrationSQL(); const statements = splitSQLStatements(sql); for (const stmt of statements) { console.log("Executing:", getStatementPreview(stmt)); await client.execute(stmt); } console.log("Database setup complete!"); process.exit(0); } // Only run main() if this file is executed directly (not imported) const isMainModule = import.meta.url === `file://${process.argv[1]}`; if (isMainModule) { main().catch((err) => { console.error("Migration failed:", err); process.exit(1); }); }