ba3ebd27f4
- Add 402 unit tests with 61.7% code coverage - Add Vitest configuration with coverage reporting - Extract testable utility functions from services - Create test.yml workflow (runs on PR and push to main) - Update docker-build.yml to require tests before building - Add scheduler-utils.ts and server-config.ts for testable code Test files added: - auth.test.ts, medications.test.ts, planner.test.ts - settings.test.ts, doses.test.ts, share.test.ts - database.test.ts, server.test.ts, services.test.ts - env.test.ts, translations.test.ts, integration.test.ts - e2e-routes.test.ts, stock-calculation.test.ts
173 lines
5.9 KiB
TypeScript
173 lines
5.9 KiB
TypeScript
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);
|
|
});
|
|
}
|