import { mkdir } from "node:fs/promises"; import { Database, SQLiteError } from "bun:sqlite"; import { UserEntry, RecordEntry } from "$lib/db_types"; import { calculateDuration, parseDate, isTimeValidHHMM } from "$lib/util"; const DATABASES_PATH: string = ""; const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite"; const CHECK_QUERY: string = "SELECT * FROM sqlite_master;"; const USER_DATABASE_SETUP: string[] = [ "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT, initials TEXT, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL);", ] const USER_DATABASE_ADD_USER: string = "INSERT INTO users (name, initials) VALUES ($name, $initials);"; const USER_DATABASE_GET_USER: string = "SELECT * FROM users;"; const ENTRY_DATABASE_SETUP: string[] = [ "PRAGMA foreign_keys = ON;", "CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);", "INSERT INTO meta(key, value) VALUES ('triggerActive', 1)", "CREATE TABLE records ( \ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \ date VARCHAR(10), \ start VARCHAR(5), \ end VARCHAR(5), \ comment TEXT, \ created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \ modified DATETIME DEFAULT NULL, \ modified_to INTEGER UNIQUE DEFAULT NULL, \ FOREIGN KEY(modified_to) REFERENCES records(id) \ );", `CREATE TRIGGER prevent_update_if_superseded BEFORE UPDATE ON records WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 AND (OLD.modified_to NOT NULL OR OLD.date ISNULL) BEGIN SELECT raise(ABORT, 'Modification on changed row is not allowed'); END;`, "CREATE TRIGGER prevent_update \ BEFORE UPDATE ON records \ WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 \ BEGIN \ INSERT INTO records(date, start, end, comment) VALUES (NEW.date, NEW.start, NEW.end, NEW.comment); \ UPDATE records SET (modified, modified_to) = (CURRENT_TIMESTAMP, last_insert_rowid()) WHERE NEW.id == id; \ SELECT raise(IGNORE); \ END;", `CREATE TRIGGER prevent_delete BEFORE DELETE ON records BEGIN UPDATE records SET (date, start, end, comment) = (null, null, null, null) WHERE OLD.id = id; SELECT raise(IGNORE); END;`, `CREATE TABLE estimates ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, year INTEGER NOT NULL, month INTEGER NOT NULL, estimate REAL NOT NULL, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, modified DATETIME DEFAULT NULL, modified_to INTEGER UNIQUE DEFAULT NULL, FOREIGN KEY(modified_to) REFERENCES estimates(id) );`, `CREATE TRIGGER estimates_prevent_duplicates BEFORE INSERT ON estimates WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 AND EXISTS(SELECT 1 FROM estimates WHERE year = NEW.year AND month = NEW.month) BEGIN SELECT raise (ABORT, 'Prevented INSERT of duplicate row'); END;`, `CREATE TRIGGER estimates_prevent_update_if_superseded BEFORE UPDATE ON estimates WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 AND (OLD.modified_to NOT NULL) BEGIN SELECT raise(ABORT, 'Modification on changed row is not allowed'); END;`, `CREATE TRIGGER estimates_prevent_update BEFORE UPDATE ON estimates WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 BEGIN INSERT INTO estimates(year, month, estimate) VALUES (NEW.year, NEW.month, NEW.estimate); UPDATE estimates SET (modified, modified_to) = (CURRENT_TIMESTAMP, last_insert_rowid()) WHERE NEW.id == id; SELECT raise(IGNORE); END;`, `CREATE TRIGGER estimates_prevent_delete BEFORE DELETE ON estimates BEGIN SELECT raise(ABORT, 'DELETE is not allowed on this table'); END;`, ] const ENTRY_DATABASE_GET_MONTHS: string = "SELECT DISTINCT SUBSTR(date, 7, 4) as year, SUBSTR(date, 4, 2) as month FROM records ORDER BY year DESC, month DESC;" const ENTRY_DATABASE_GET_ENTRY_BY_ID: string = "SELECT * FROM records WHERE modified_to ISNULL AND id = $id;" const ENTRY_DATABASE_GET_ENTRIES_IN_MONTH: string = "SELECT * FROM records WHERE modified_to ISNULL AND SUBSTR(date, 7, 4) = $year AND SUBSTR(date, 4, 2) = $month ORDER BY SUBSTR(date, 1, 2);" const ENTRY_DATABASE_GET_ENTRIES: string = "SELECT * FROM records WHERE modified_to ISNULL ORDER BY SUBSTR(date, 7, 4) DESC, SUBSTR(date, 4, 2) DESC, SUBSTR(date, 1, 2) DESC;" const ENTRY_DATABASE_ADD_ENTRY: string = "INSERT INTO records(date, start, end, comment) VALUES ($date, $start, $end, $comment);" const ENTRY_DATABASE_EDIT_ENTRY: string = "UPDATE records SET date = $date, start = $start, end = $end, comment = $comment WHERE id = $id;"; export class User { id: number; name: string; created: string; private database: Database; constructor(user: UserEntry, db: Database) { this.id = user.id; this.name = user.name; this.created = user.created; this._database = db; } get_months(): { year: number, month: number }[] { const query = this._database.query(ENTRY_DATABASE_GET_MONTHS); const res = query.all(); return res; } get_hr_sum(year: number, month: number): number { const months = this.get_entries_by_month(year, month); let sum = 0; months.forEach((record) => { sum += calculateDuration(record.start, record.end) }) return sum; } get_entries(): Entry[] { const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES); const res = query.all() return res; } get_entries_by_month(year: number, month: number): Entry[] { if (!(month > 0 && month < 13)) { return []; } const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES_IN_MONTH); const res = query.all({ year: year.toString(), month: month.toString().padStart(2, '0') }); return res; } get_entry(id: number): Entry { const query = this._database.query(ENTRY_DATABASE_GET_ENTRY_BY_ID); const res = query.get({ id: id }); return res; } insert_entry(date: string, start: string, end: string, comment: string | null) { if (parseDate(date) == null || !isTimeValidHHMM(start) || !isTimeValidHHMM(end)) { return false; } const query = this._database.query(ENTRY_DATABASE_ADD_ENTRY); const res = query.run({ date: date, start: start, end: end, comment: comment }); return res.changes == 1; } update_entry(id: number, ndate: string, nstart: string, nend: string, ncomment: string): RecordEntry | null { if (isNaN(id) || parseDate(ndate) == null || !isTimeValidHHMM(nstart) || !isTimeValidHHMM(nend)) { return null; } const query = this._database.query(ENTRY_DATABASE_EDIT_ENTRY); const res = query.run({ id: id, date: ndate, start: nstart, end: nend, comment: ncomment }); return res.changes > 1; } } let user_database: Database; function is_db_initialized(db: Database): boolean { try { let res = db.query(CHECK_QUERY).get(); return res != null; } catch (exception) { if (!(exception instanceof SQLiteError)) { throw exception; } console.log(e); return false; } } function get_user_db_name(user: UserEntry) { return DATABASES_PATH + "user-" + user.id + ".sqlite" } function setup_db(db: Database, setup_queries: string[]) { setup_queries.forEach((q) => { db.query(q).run(); }); } export function init_db() { user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true }); if (!is_db_initialized(user_database)) { setup_db(user_database, USER_DATABASE_SETUP); } } export function close_db() { if (user_database) { user_database.close(); } } export function create_user(name: string, initials: string): boolean { try { const statement = user_database.query(USER_DATABASE_ADD_USER); const result = statement.run({ name: name, initials: initials }); return true; } catch (e) { console.log(e); if (e instanceof SQLiteError) { return false; } throw e; } } function _get_user(): UserEntry | null { try { const statement = user_database.prepare(USER_DATABASE_GET_USER); const result: UserEntry = statement.get(); if (result == null) { create_user("Patrick Maschek", "PM"); return _get_user(); } return result; } catch (e) { if (e instanceof SQLiteError) { return null; } throw e; } } export function get_user(): User | null { const user = _get_user(); if (user == null) { return null; } const db_name = get_user_db_name(user); try { let userdb = new Database(db_name, { create: true, strict: true }); if (!is_db_initialized(userdb)) { setup_db(userdb, ENTRY_DATABASE_SETUP); } return new User(user, userdb); } catch (e) { console.log(e); return null; } }