2025-01-03 01:04:30 +01:00
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" ;
2025-01-03 01:04:30 +01:00
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);" ,
2025-01-03 01:04:30 +01:00
]
const USER_DATABASE_ADD_USER : string =
2025-01-17 13:32:28 +01:00
"INSERT INTO users (name, initials) VALUES ($name, $initials);" ;
2025-01-03 01:04:30 +01:00
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-03 01:04:30 +01:00
]
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;"
2025-01-03 01:04:30 +01:00
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);"
2025-01-03 01:04:30 +01:00
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;"
2025-01-03 01:04:30 +01:00
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 =
2025-01-03 01:04:30 +01:00
"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 ;
2025-01-03 01:04:30 +01:00
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 ;
2025-01-03 01:04:30 +01:00
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 ;
}
2025-01-03 01:04:30 +01:00
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' ) } ) ;
2025-01-03 01:04:30 +01:00
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 ) ;
2025-01-03 01:04:30 +01:00
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 {
2025-01-03 01:04:30 +01:00
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 } ) ;
2025-01-03 01:04:30 +01:00
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 {
2025-01-03 01:04:30 +01:00
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 ( ) ;
2025-01-03 01:04:30 +01:00
}
return result ;
} catch ( e ) {
if ( e instanceof SQLiteError ) {
2025-01-17 13:32:28 +01:00
return null ;
2025-01-03 01:04:30 +01:00
}
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 ;
}
2025-01-03 01:04:30 +01:00
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 ;
}
}