Stundenaufzeichnung/src/lib/server/database.ts

315 lines
8.3 KiB
TypeScript
Raw Normal View History

import { mkdir } from "node:fs/promises";
import { Database, SQLiteError } from "bun:sqlite";
import { UserEntry, RecordEntry } from "$lib/db_types";
2025-01-17 13:32:28 +01:00
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[] = [
2025-01-17 13:32:28 +01:00
"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 =
2025-01-17 13:32:28 +01:00
"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);
2025-01-17 13:32:28 +01:00
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;`,
]
2025-01-17 13:32:28 +01:00
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;"
2025-01-17 13:32:28 +01:00
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 =
2025-01-17 13:32:28 +01:00
"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);"
2025-01-17 13:32:28 +01:00
const ENTRY_DATABASE_EDIT_ENTRY: string =
"UPDATE records SET date = $date, start = $start, end = $end, comment = $comment WHERE id = $id;";
export class User {
2025-01-17 13:32:28 +01:00
id: number;
name: string;
created: string;
private database: Database;
constructor(user: UserEntry, db: Database) {
2025-01-17 13:32:28 +01:00
this.id = user.id;
this.name = user.name;
this.created = user.created;
this._database = db;
}
2025-01-17 13:32:28 +01:00
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()
2025-01-17 13:32:28 +01:00
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;
}
2025-01-17 13:32:28 +01:00
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();
}
}
2025-01-17 13:32:28 +01:00
export function create_user(name: string, initials: string): boolean {
try {
const statement = user_database.query(USER_DATABASE_ADD_USER);
2025-01-17 13:32:28 +01:00
const result = statement.run({ name: name, initials: initials });
return true;
} catch (e) {
console.log(e);
if (e instanceof SQLiteError) {
return false;
}
throw e;
}
}
2025-01-17 13:32:28 +01:00
function _get_user(): UserEntry | null {
try {
const statement = user_database.prepare(USER_DATABASE_GET_USER);
const result: UserEntry = statement.get();
if (result == null) {
2025-01-17 13:32:28 +01:00
create_user("Patrick Maschek", "PM");
return _get_user();
}
return result;
} catch (e) {
if (e instanceof SQLiteError) {
2025-01-17 13:32:28 +01:00
return null;
}
throw e;
}
}
export function get_user(): User | null {
const user = _get_user();
2025-01-17 13:32:28 +01:00
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;
}
}