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)" ,
2025-01-30 12:31:25 +01:00
` 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
) ; ` ,
` CREATE TABLE records_history (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,
record_id INTEGER NOT NULL ,
date VARCHAR ( 10 ) ,
start VARCHAR ( 5 ) ,
end VARCHAR ( 5 ) ,
comment TEXT ,
modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ,
FOREIGN KEY ( record_id ) REFERENCES records ( id )
) ; ` ,
` CREATE TRIGGER records_update_history
2025-01-03 01:04:30 +01:00
BEFORE UPDATE ON records
WHEN ( SELECT value FROM meta WHERE key = 'triggerActive' ) = 1
BEGIN
2025-01-30 12:31:25 +01:00
INSERT INTO records_history ( record_id , date , start , end , comment ) VALUES ( OLD . id , OLD . date , OLD . start , OLD . end , OLD . comment ) ;
2025-01-03 01:04:30 +01:00
END ; ` ,
2025-01-30 12:31:25 +01:00
` CREATE TRIGGER records_delete_history
2025-01-03 01:04:30 +01:00
BEFORE DELETE ON records
2025-01-30 12:31:25 +01:00
WHEN ( SELECT value FROM meta WHERE key = 'triggerActive' ) = 1
BEGIN
INSERT INTO records_history ( record_id , date , start , end , comment ) VALUES ( OLD . id , OLD . date , OLD . start , OLD . end , OLD . comment ) ;
2025-01-17 13:32:28 +01:00
END ; ` ,
` CREATE TABLE estimates (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,
year INTEGER NOT NULL ,
2025-01-30 12:31:25 +01:00
quarter INTEGER NOT NULL ,
estimate_0 REAL ,
estimate_1 REAL ,
estimate_2 REAL ,
2025-01-17 13:32:28 +01:00
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ,
2025-01-30 12:31:25 +01:00
UNIQUE ( year , quarter )
2025-01-17 13:32:28 +01:00
) ; ` ,
2025-01-30 12:31:25 +01:00
` CREATE TABLE estimates_history (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,
estimate_id INTEGER NOT NULL ,
year INTEGER NOT NULL ,
quarter INTEGER NOT NULL ,
estimate_0 REAL NOT NULL ,
estimate_1 REAL NOT NULL ,
estimate_2 REAL NOT NULL ,
modified DATETIME DEFAULT NULL ,
FOREIGN KEY ( estimate_id ) REFERENCES estimates ( id )
) ; ` ,
2025-01-17 13:32:28 +01:00
2025-01-30 12:31:25 +01:00
` CREATE TRIGGER estimates_update_history
2025-01-17 13:32:28 +01:00
BEFORE UPDATE ON estimates
WHEN ( SELECT value FROM meta WHERE key = 'triggerActive' ) = 1
BEGIN
2025-01-30 12:31:25 +01:00
INSERT INTO estimates_history ( estimate_id , year , quarter , estimate_0 , estimate_1 , estimate_2 ) VALUES ( OLD . id , OLD . year , OLD . quarter , OLD . estimate_0 , OLD . estimate_1 , OLD . estimate_2 ) ;
2025-01-17 13:32:28 +01:00
END ; ` ,
2025-01-30 12:31:25 +01:00
` CREATE TRIGGER estimates_delete_history
2025-01-17 13:32:28 +01:00
BEFORE DELETE ON estimates
2025-01-30 12:31:25 +01:00
WHEN ( SELECT value FROM meta WHERE key = 'triggerActive' ) = 1
BEGIN
INSERT INTO estimates_history ( estimate_id , year , quarter , estimate_0 , estimate_1 , estimate_2 ) VALUES ( OLD . id , OLD . year , OLD . quarter , OLD . estimate_0 , OLD . estimate_1 , OLD . estimate_2 ) ;
2025-01-17 13:32:28 +01:00
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 =
2025-01-30 12:31:25 +01:00
"SELECT * FROM records WHERE id = $id;"
2025-01-03 01:04:30 +01:00
2025-01-17 13:32:28 +01:00
const ENTRY_DATABASE_GET_ENTRIES_IN_MONTH : string =
2025-01-30 12:31:25 +01:00
"SELECT * FROM records WHERE SUBSTR(date, 7, 4) = $year AND SUBSTR(date, 4, 2) = $month ORDER BY SUBSTR(date, 1, 2);"
2025-01-17 13:32:28 +01:00
2025-01-03 01:04:30 +01:00
const ENTRY_DATABASE_GET_ENTRIES : string =
2025-01-30 12:31:25 +01:00
"SELECT * FROM records 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;" ;
2025-01-30 12:31:25 +01:00
const ESTIMATES_DATABASE_GET_ALL : string =
"SELECT * FROM estimates ORDER BY year DESC, quarter DESC;"
const ESTIMATES_DATABASE_GET_QUARTERS : string =
"SELECT year, quarter FROM estimates;"
const ESTIMATES_DATABASE_GET_MONTH : string =
"SELECT estimate_0, estimate_1, estimate_2 FROM estimates WHERE year = $year AND quarter = $quarter;"
const ESTIMATES_DATABASE_INSERT : string =
"INSERT INTO estimates(year, quarter, estimate_0, estimate_1, estimate_2) VALUES ($year, $quarter, $estimate_0, $estimate_1, $estimate_2);"
2025-01-03 01:04:30 +01:00
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-30 12:31:25 +01:00
get_months ( ) : { year : string , month : string } [ ] {
2025-01-17 13:32:28 +01:00
const query = this . _database . query ( ENTRY_DATABASE_GET_MONTHS ) ;
const res = query . all ( ) ;
return res ;
}
2025-01-30 12:31:25 +01:00
get_quarters ( ) : { year : number , quarter : number } [ ] {
const query = this . _database . query ( ESTIMATES_DATABASE_GET_QUARTERS )
const res = query . all ( ) ;
return res ;
}
2025-01-17 13:32:28 +01:00
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 ;
}
2025-01-30 12:31:25 +01:00
get_entries_by_month ( year : number , month : number ) : RecordEntry [ ] {
2025-01-17 13:32:28 +01:00
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 ;
}
2025-01-30 12:31:25 +01:00
get_entry ( id : number ) : RecordEntry {
2025-01-03 01:04:30 +01:00
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 ;
}
2025-01-30 12:31:25 +01:00
update_entry ( id : number , ndate : string , nstart : string , nend : string , ncomment : string ) : boolean {
2025-01-03 01:04:30 +01:00
if ( isNaN ( id ) || parseDate ( ndate ) == null || ! isTimeValidHHMM ( nstart ) || ! isTimeValidHHMM ( nend ) ) {
2025-01-30 12:31:25 +01:00
return false ;
2025-01-03 01:04:30 +01:00
}
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 ;
}
2025-01-30 12:31:25 +01:00
get_estimates ( ) : Array < EstimatesEntry > {
const query = this . _database . query ( ESTIMATES_DATABASE_GET_ALL ) ;
const res = query . all ( ) ;
return res ;
}
get_estimate ( year : number , month : number ) : number {
const query = this . _database . query ( ESTIMATES_DATABASE_GET_MONTH ) ;
const res = query . all ( { year : year , quarter : Math.floor ( month / 4 + 1 ) } ) ;
return res [ 0 ] ? . [ ` estimate_ ${ month % 3 } ` ] ? ? NaN ;
}
insert_estimate ( year : number , quarter : number , estimate_0 : number , estimate_1 : number , estimate_2 : number ) {
if ( isNaN ( year ) || isNaN ( quarter ) || quarter < 1 || quarter > 4 || isNaN ( estimate_0 ) || isNaN ( estimate_1 ) || isNaN ( estimate_2 ) ) {
return null ;
}
const query = this . _database . query ( ESTIMATES_DATABASE_INSERT ) ;
const res = query . run ( { year : year , quarter : quarter , estimate_0 : estimate_0 , estimate_1 : estimate_1 , estimate_2 : estimate_2 } ) ;
return res . changes > 1 ;
}
2025-01-03 01:04:30 +01:00
}
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 [ ] ) {
2025-01-30 12:31:25 +01:00
setup_queries . forEach ( ( q ) = > { console . log ( q ) ; db . query ( q ) . run ( ) ; } ) ;
2025-01-03 01:04:30 +01:00
}
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 ;
}
}