2026-07-16 20:13:49 +08:00
package database
import (
"context"
"database/sql"
"fmt"
"strings"
"sync"
_ "github.com/mattn/go-sqlite3"
"github.com/yyc12345/coconut-leaf/backend/dt"
"github.com/yyc12345/coconut-leaf/backend/utils"
)
// sqliteSchema is the DDL of the six tables, inlined from the legacy
// sql/sqlite.sql (ccn_-free snake_case column names). Each entry is executed as
// a single statement in Init.
var sqliteSchema = [ ] string {
` CREATE TABLE user (
[ name ] TEXT NOT NULL ,
[ password ] TEXT NOT NULL ,
[ is_admin ] TINYINT NOT NULL CHECK ( is_admin = 1 OR is_admin = 0 ) ,
[ salt ] INTEGER NOT NULL ,
PRIMARY KEY ( name )
) ; ` ,
` CREATE TABLE token (
[ user ] TEXT NOT NULL ,
[ token ] TEXT UNIQUE NOT NULL ,
[ token_expire_on ] BIGINT NOT NULL ,
[ ua ] TEXT NOT NULL ,
[ ip ] TEXT NOT NULL ,
FOREIGN KEY ( user ) REFERENCES user ( name ) ON DELETE CASCADE
) ; ` ,
` CREATE TABLE collection (
[ uuid ] TEXT NOT NULL ,
[ name ] TEXT NOT NULL ,
[ user ] TEXT NOT NULL ,
[ last_change ] TEXT NOT NULL ,
PRIMARY KEY ( uuid ) ,
FOREIGN KEY ( user ) REFERENCES user ( name ) ON DELETE CASCADE
) ; ` ,
` CREATE TABLE share (
[ uuid ] TEXT NOT NULL ,
[ target ] TEXT NOT NULL ,
FOREIGN KEY ( uuid ) REFERENCES collection ( uuid ) ON DELETE CASCADE
FOREIGN KEY ( target ) REFERENCES user ( name ) ON DELETE CASCADE
) ; ` ,
` CREATE TABLE calendar (
[ uuid ] TEXT NOT NULL ,
[ belong_to ] TEXT NOT NULL ,
[ title ] TEXT NOT NULL ,
[ description ] TEXT NOT NULL ,
[ last_change ] TEXT NOT NULL ,
[ event_date_time_start ] BIGINT NOT NULL ,
[ event_date_time_end ] BIGINT NOT NULL ,
[ timezone_offset ] INT NOT NULL ,
[ loop_rules ] TEXT NOT NULL ,
[ loop_date_time_start ] BIGINT NOT NULL ,
[ loop_date_time_end ] BIGINT NOT NULL ,
PRIMARY KEY ( uuid ) ,
FOREIGN KEY ( belong_to ) REFERENCES collection ( uuid ) ON DELETE CASCADE
) ; ` ,
` CREATE TABLE todo (
[ uuid ] TEXT NOT NULL ,
[ belong_to ] TEXT NOT NULL ,
[ data ] TEXT NOT NULL ,
[ last_change ] TEXT NOT NULL ,
PRIMARY KEY ( uuid ) ,
FOREIGN KEY ( belong_to ) REFERENCES user ( name ) ON DELETE CASCADE
) ; ` ,
}
// dbOp is the unit of database work executed inside a safeOp transaction.
type dbOp func ( ctx context . Context , tx * sql . Tx ) ( any , error )
// Sqlite3Database implements Database backed by a SQLite file. It owns the
// connection, a serializing mutex (SQLite is driven single-threaded here, as in
// the legacy code) and the auto token-clean bookkeeping.
type Sqlite3Database struct {
deps Deps
db * sql . DB
mu sync . Mutex
latestClean int
}
// NewSqlite3Database opens the SQLite file and applies the legacy open() PRAGMAs
// (UTF-8 encoding, foreign keys). A single connection is used to mirror the
// legacy single-connection model and keep PRAGMAs effective.
func NewSqlite3Database ( deps Deps ) ( * Sqlite3Database , error ) {
db , err := sql . Open ( "sqlite3" , deps . Cfg . Database . Sqlite . Path )
if err != nil {
return nil , err
}
db . SetMaxOpenConns ( 1 )
if _ , err := db . Exec ( ` PRAGMA encoding = "UTF-8"; ` ) ; err != nil {
db . Close ( )
return nil , err
}
if _ , err := db . Exec ( ` PRAGMA foreign_keys = ON; ` ) ; err != nil {
db . Close ( )
return nil , err
}
if err := db . Ping ( ) ; err != nil {
db . Close ( )
return nil , err
}
return & Sqlite3Database { deps : deps , db : db } , nil
}
// Init creates the schema and the first admin user, mirroring the legacy init().
func ( d * Sqlite3Database ) Init ( username , password string ) error {
d . mu . Lock ( )
defer d . mu . Unlock ( )
tx , err := d . db . Begin ( )
if err != nil {
return err
}
for _ , stmt := range sqliteSchema {
if _ , err := tx . Exec ( stmt ) ; err != nil {
tx . Rollback ( )
return err
}
}
if _ , err := tx . Exec ( "INSERT INTO user VALUES (?, ?, ?, ?);" , username , utils . ComputePasswordHash ( password ) , 1 , utils . GenerateSalt ( ) ) ; err != nil {
tx . Rollback ( )
return err
}
return tx . Commit ( )
}
// Close closes the underlying connection.
func ( d * Sqlite3Database ) Close ( ) error {
return d . db . Close ( )
}
// safeOp runs fn inside a serialized transaction. It reproduces the legacy
// SafeDatabaseOperation decorator: lock, begin, periodically purge outdated
// tokens (logging when it does), run the work, commit on success / rollback on
// error. DB errors are logged only in debug mode, matching the legacy
// `if cfg.others.debug: LOGGER.exception(e)` behavior.
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) safeOp ( ctx context . Context , fn dbOp ) ( any , error ) {
2026-07-16 20:13:49 +08:00
d . mu . Lock ( )
defer d . mu . Unlock ( )
debug := d . deps . Cfg . Others . Debug
logger := d . deps . Logger
tx , err := d . db . BeginTx ( ctx , nil )
if err != nil {
if debug {
logger . Error ( "failed to begin transaction" , "error" , err )
}
2026-07-16 20:51:18 +08:00
return nil , err
2026-07-16 20:13:49 +08:00
}
if now := utils . GetCurrentTimestamp ( ) ; now - d . latestClean > d . deps . Cfg . Others . AutoTokenCleanDuration {
d . latestClean = now
logger . Info ( "Cleaning outdated token..." )
if err := tokenOperClean ( ctx , tx ) ; err != nil {
tx . Rollback ( )
if debug {
logger . Error ( "failed to clean outdated tokens" , "error" , err )
}
2026-07-16 20:51:18 +08:00
return nil , err
2026-07-16 20:13:49 +08:00
}
}
data , err := fn ( ctx , tx )
if err != nil {
tx . Rollback ( )
if debug {
logger . Error ( "database operation failed" , "error" , err )
}
2026-07-16 20:51:18 +08:00
return nil , err
2026-07-16 20:13:49 +08:00
}
if err := tx . Commit ( ) ; err != nil {
if debug {
logger . Error ( "failed to commit transaction" , "error" , err )
}
2026-07-16 20:51:18 +08:00
return nil , err
2026-07-16 20:13:49 +08:00
}
2026-07-16 20:51:18 +08:00
return data , nil
2026-07-16 20:13:49 +08:00
}
// region: Token-related Internal Operations
// tokenOperClean deletes outdated tokens. Mirrors tokenOper_clean.
func tokenOperClean ( ctx context . Context , tx * sql . Tx ) error {
_ , err := tx . ExecContext ( ctx , "DELETE FROM token WHERE [token_expire_on] <= ?;" , utils . GetCurrentTimestamp ( ) )
return err
}
// tokenOperPostponeExpireOn pushes the token's expiry forward. Mirrors
// tokenOper_postpone_expireOn.
func tokenOperPostponeExpireOn ( ctx context . Context , tx * sql . Tx , token string ) error {
_ , err := tx . ExecContext ( ctx , "UPDATE token SET [token_expire_on] = ? WHERE [token] = ?;" , utils . GetTokenExpireOn ( ) , token )
return err
}
// tokenOperGetUsername validates token, postpones its expiry, and returns the
// owning username. Mirrors tokenOper_get_username.
func tokenOperGetUsername ( ctx context . Context , tx * sql . Tx , token string ) ( string , error ) {
var user string
err := tx . QueryRowContext ( ctx , "SELECT [user] FROM token WHERE [token] = ? AND [token_expire_on] > ?;" , token , utils . GetCurrentTimestamp ( ) ) . Scan ( & user )
if err != nil {
return "" , err
}
if err := tokenOperPostponeExpireOn ( ctx , tx , token ) ; err != nil {
return "" , err
}
return user , nil
}
// tokenOperCheckValid validates a token (mirrors tokenOper_check_valid).
func tokenOperCheckValid ( ctx context . Context , tx * sql . Tx , token string ) error {
_ , err := tokenOperGetUsername ( ctx , tx , token )
return err
}
// tokenOperIsAdmin reports whether the user is an admin (mirrors
// tokenOper_is_admin).
func tokenOperIsAdmin ( ctx context . Context , tx * sql . Tx , username string ) ( bool , error ) {
var isAdmin int64
err := tx . QueryRowContext ( ctx , "SELECT [is_admin] FROM user WHERE [name] = ?;" , username ) . Scan ( & isAdmin )
if err != nil {
return false , err
}
return isAdmin == 1 , nil
}
// endregion
// region: Operation Functions
// region: Common Namespace
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CommonSalt ( ctx context . Context , username string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
salt := utils . GenerateSalt ( )
if _ , err := tx . ExecContext ( ctx , "UPDATE user SET [salt] = ? WHERE [name] = ?;" , salt , username ) ; err != nil {
return nil , err
}
return salt , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CommonLogin ( ctx context . Context , username , password , clientUa , clientIp string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
var gottenPassword string
var gottenSalt int64
err := tx . QueryRowContext ( ctx , "SELECT [password], [salt] FROM user WHERE [name] = ?;" , username ) . Scan ( & gottenPassword , & gottenSalt )
if err != nil {
return nil , fmt . Errorf ( "Login authentication failed" )
}
if password == utils . ComputePasswordHashWithSalt ( gottenPassword , int ( gottenSalt ) ) {
token := utils . GenerateToken ( username )
if _ , err := tx . ExecContext ( ctx , "UPDATE user SET [salt] = ? WHERE [name] = ?;" , utils . GenerateSalt ( ) , username ) ; err != nil {
return nil , err
}
if _ , err := tx . ExecContext ( ctx , "INSERT INTO token VALUES (?, ?, ?, ?, ?);" , username , token , utils . GetTokenExpireOn ( ) , clientUa , clientIp ) ; err != nil {
return nil , err
}
return token , nil
}
return nil , fmt . Errorf ( "Login authentication failed" )
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CommonWebLogin ( ctx context . Context , username , password , clientUa , clientIp string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
logger := d . deps . Logger
logger . Debug ( "WebLogin username" , "username" , username )
logger . Debug ( "WebLogin password" , "password" , password )
passwordHash := utils . ComputePasswordHash ( password )
logger . Debug ( "WebLogin password hash" , "hash" , passwordHash )
var name string
err := tx . QueryRowContext ( ctx , "SELECT [name] FROM user WHERE [name] = ? AND [password] = ?;" , username , passwordHash ) . Scan ( & name )
if err != nil {
return nil , fmt . Errorf ( "Login authentication failed" )
}
token := utils . GenerateToken ( username )
if _ , err := tx . ExecContext ( ctx , "INSERT INTO token VALUES (?, ?, ?, ?, ?);" , username , token , utils . GetTokenExpireOn ( ) , clientUa , clientIp ) ; err != nil {
return nil , err
}
return token , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CommonLogout ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
if _ , err := tx . ExecContext ( ctx , "DELETE FROM token WHERE [token] = ?;" , token ) ; err != nil {
return nil , err
}
return true , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CommonTokenValid ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
return true , nil
} )
}
// endregion
// region: Calendar Namespace
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CalendarGetFull ( ctx context . Context , token string , startDateTime , endDateTime int64 ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , ` SELECT calendar . * FROM calendar INNER JOIN collection
ON collection . uuid = calendar . belong_to
WHERE ( collection . user = ? AND calendar . loop_date_time_end >= ? AND calendar . loop_date_time_start - ( calendar . event_date_time_end - calendar . event_date_time_start ) <= ? ) ; ` ,
username , startDateTime , endDateTime )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] [ ] any { }
for rows . Next ( ) {
var uuid , belongTo , title , description , lastChange , loopRules string
var eventStart , eventEnd , tzOffset , loopStart , loopEnd int64
if err := rows . Scan ( & uuid , & belongTo , & title , & description , & lastChange , & eventStart , & eventEnd , & tzOffset , & loopRules , & loopStart , & loopEnd ) ; err != nil {
return nil , err
}
result = append ( result , [ ] any { uuid , belongTo , title , description , lastChange , eventStart , eventEnd , tzOffset , loopRules , loopStart , loopEnd } )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CalendarGetList ( ctx context . Context , token string , startDateTime , endDateTime int64 ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , ` SELECT calendar . uuid FROM calendar INNER JOIN collection
ON collection . uuid = calendar . belong_to
WHERE ( collection . user = ? AND calendar . loop_date_time_end >= ? AND calendar . loop_date_time_start - ( calendar . event_date_time_end - calendar . event_date_time_start ) <= ? ) ; ` ,
username , startDateTime , endDateTime )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] string { }
for rows . Next ( ) {
var uuid string
if err := rows . Scan ( & uuid ) ; err != nil {
return nil , err
}
result = append ( result , uuid )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CalendarGetDetail ( ctx context . Context , token , uuid string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
var dUuid , dBelongTo , dTitle , dDescription , dLastChange , dLoopRules string
var dEventStart , dEventEnd , dTzOffset , dLoopStart , dLoopEnd int64
err := tx . QueryRowContext ( ctx , "SELECT * FROM calendar WHERE [uuid] = ?;" , uuid ) .
Scan ( & dUuid , & dBelongTo , & dTitle , & dDescription , & dLastChange , & dEventStart , & dEventEnd , & dTzOffset , & dLoopRules , & dLoopStart , & dLoopEnd )
if err != nil {
if err == sql . ErrNoRows {
return nil , nil
}
return nil , err
}
return [ ] any { dUuid , dBelongTo , dTitle , dDescription , dLastChange , dEventStart , dEventEnd , dTzOffset , dLoopRules , dLoopStart , dLoopEnd } , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CalendarUpdate ( ctx context . Context , token , uuid , lastChange string , opts CalendarUpdateOptions ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
// get prev data
var uUuid , uBelongTo , uTitle , uDescription , uLastChange , uLoopRules string
var uEventStart , uEventEnd , uTzOffset , uLoopStart , uLoopEnd int64
err := tx . QueryRowContext ( ctx , "SELECT * FROM calendar WHERE [uuid] = ? AND [last_change] = ?;" , uuid , lastChange ) .
Scan ( & uUuid , & uBelongTo , & uTitle , & uDescription , & uLastChange , & uEventStart , & uEventEnd , & uTzOffset , & uLoopRules , & uLoopStart , & uLoopEnd )
if err != nil {
return nil , err
}
// construct update data
newLastUpdate := utils . GenerateUUID ( )
sqlList := [ ] string { "[last_change] = ?" }
args := [ ] any { newLastUpdate }
// analyse opt arg
reAnalyseLoop := false
effEventStart , effTzOffset , effLoopRules := uEventStart , uTzOffset , uLoopRules
if opts . BelongTo != nil {
sqlList = append ( sqlList , "[belong_to] = ?" )
args = append ( args , * opts . BelongTo )
}
if opts . Title != nil {
sqlList = append ( sqlList , "[title] = ?" )
args = append ( args , * opts . Title )
}
if opts . Description != nil {
sqlList = append ( sqlList , "[description] = ?" )
args = append ( args , * opts . Description )
}
if opts . EventDateTimeStart != nil {
sqlList = append ( sqlList , "[event_date_time_start] = ?" )
args = append ( args , * opts . EventDateTimeStart )
reAnalyseLoop = true
effEventStart = * opts . EventDateTimeStart
}
if opts . EventDateTimeEnd != nil {
sqlList = append ( sqlList , "[event_date_time_end] = ?" )
args = append ( args , * opts . EventDateTimeEnd )
}
if opts . LoopRules != nil {
sqlList = append ( sqlList , "[loop_rules] = ?" )
args = append ( args , * opts . LoopRules )
reAnalyseLoop = true
effLoopRules = * opts . LoopRules
}
if opts . TimezoneOffset != nil {
sqlList = append ( sqlList , "[timezone_offset] = ?" )
args = append ( args , * opts . TimezoneOffset )
reAnalyseLoop = true
effTzOffset = * opts . TimezoneOffset
}
if reAnalyseLoop {
// re-compute loop data and upload it into list
sqlList = append ( sqlList , "[loop_date_time_start] = ?" )
args = append ( args , effEventStart )
sqlList = append ( sqlList , "[loop_date_time_end] = ?" )
loopEnd , err := dt . ResolveLoopStr ( effLoopRules , effEventStart , effTzOffset )
if err != nil {
return nil , err
}
args = append ( args , loopEnd )
}
args = append ( args , uuid )
query := "UPDATE calendar SET " + strings . Join ( sqlList , ", " ) + " WHERE [uuid] = ?;"
res , err := tx . ExecContext ( ctx , query , args ... )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to update due to no matched rows or too much rows." )
}
return newLastUpdate , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CalendarAdd ( ctx context . Context , token , belongTo , title , description string , eventDateTimeStart , eventDateTimeEnd int64 , loopRules string , timezoneOffset int64 ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
newUuid := utils . GenerateUUID ( )
newLastUpdate := utils . GenerateUUID ( )
loopDateTimeStart := eventDateTimeStart
loopDateTimeEnd , err := dt . ResolveLoopStr ( loopRules , eventDateTimeStart , timezoneOffset )
if err != nil {
return nil , err
}
if _ , err := tx . ExecContext ( ctx , "INSERT INTO calendar VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" ,
newUuid , belongTo , title , description , newLastUpdate ,
eventDateTimeStart , eventDateTimeEnd , timezoneOffset , loopRules ,
loopDateTimeStart , loopDateTimeEnd ) ; err != nil {
return nil , err
}
return newUuid , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CalendarDelete ( ctx context . Context , token , uuid , lastChange string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
res , err := tx . ExecContext ( ctx , "DELETE FROM calendar WHERE [uuid] = ? AND [last_change] = ?;" , uuid , lastChange )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
return true , nil
} )
}
// endregion
// region: Collection Namespace
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionGetFullOwn ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , "SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ?;" , username )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] [ ] any { }
for rows . Next ( ) {
var uuid , name , lastChange string
if err := rows . Scan ( & uuid , & name , & lastChange ) ; err != nil {
return nil , err
}
result = append ( result , [ ] any { uuid , name , lastChange } )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionGetListOwn ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , "SELECT [uuid] FROM collection WHERE [user] = ?;" , username )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] string { }
for rows . Next ( ) {
var uuid string
if err := rows . Scan ( & uuid ) ; err != nil {
return nil , err
}
result = append ( result , uuid )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionGetDetailOwn ( ctx context . Context , token , uuid string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
var dUuid , dName , dLastChange string
err = tx . QueryRowContext ( ctx , "SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ? AND [uuid] = ?;" , username , uuid ) . Scan ( & dUuid , & dName , & dLastChange )
if err != nil {
if err == sql . ErrNoRows {
return nil , nil
}
return nil , err
}
return [ ] any { dUuid , dName , dLastChange } , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionAddOwn ( ctx context . Context , token , name string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
newUuid := utils . GenerateUUID ( )
newLastUpdate := utils . GenerateUUID ( )
if _ , err := tx . ExecContext ( ctx , "INSERT INTO collection VALUES (?, ?, ?, ?);" , newUuid , name , username , newLastUpdate ) ; err != nil {
return nil , err
}
return newUuid , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionUpdateOwn ( ctx context . Context , token , uuid , name , lastChange string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
newLastUpdate := utils . GenerateUUID ( )
res , err := tx . ExecContext ( ctx , "UPDATE collection SET [name] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;" , name , newLastUpdate , uuid , lastChange )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to update due to no matched rows or too much rows." )
}
return newLastUpdate , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionDeleteOwn ( ctx context . Context , token , uuid , lastChange string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
res , err := tx . ExecContext ( ctx , "DELETE FROM collection WHERE [uuid] = ? AND [last_change] = ?;" , uuid , lastChange )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
return true , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionGetSharing ( ctx context . Context , token , uuid string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , "SELECT [target] FROM share WHERE [uuid] = ?;" , uuid )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] string { }
for rows . Next ( ) {
var target string
if err := rows . Scan ( & target ) ; err != nil {
return nil , err
}
result = append ( result , target )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionDeleteSharing ( ctx context . Context , token , uuid , target , lastChange string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
newLastUpdate := utils . GenerateUUID ( )
// NOTE: the legacy SQL had a stray comma ("SET [last_change] = ?, WHERE ...")
// which is a syntax error; it is corrected here.
res , err := tx . ExecContext ( ctx , "UPDATE collection SET [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;" , newLastUpdate , uuid , lastChange )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
res , err = tx . ExecContext ( ctx , "DELETE FROM share WHERE [uuid] = ? AND [target] = ?;" , uuid , target )
if err != nil {
return nil , err
}
n , err = res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
return newLastUpdate , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionAddSharing ( ctx context . Context , token , uuid , target , lastChange string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
newLastUpdate := utils . GenerateUUID ( )
res , err := tx . ExecContext ( ctx , "UPDATE collection SET [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;" , newLastUpdate , uuid , lastChange )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
var dupUuid , dupTarget string
err = tx . QueryRowContext ( ctx , "SELECT * FROM share WHERE [uuid] = ? AND [target] = ?;" , uuid , target ) . Scan ( & dupUuid , & dupTarget )
if err == nil {
return nil , fmt . Errorf ( "Fail to insert duplicated item." )
} else if err != sql . ErrNoRows {
return nil , err
}
if _ , err := tx . ExecContext ( ctx , "INSERT INTO share VALUES (?, ?);" , uuid , target ) ; err != nil {
return nil , err
}
return newLastUpdate , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) CollectionGetShared ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , ` SELECT collection . uuid , collection . name , collection . user
FROM share INNER JOIN collection
ON share . uuid = collection . uuid
WHERE share . target = ? ; ` , username )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] [ ] any { }
for rows . Next ( ) {
var uuid , name , user string
if err := rows . Scan ( & uuid , & name , & user ) ; err != nil {
return nil , err
}
result = append ( result , [ ] any { uuid , name , user } )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
// endregion
// region: Todo Namespace
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) TodoGetFull ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , "SELECT * FROM todo WHERE [belong_to] = ?;" , username )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] [ ] any { }
for rows . Next ( ) {
var uuid , belongTo , data , lastChange string
if err := rows . Scan ( & uuid , & belongTo , & data , & lastChange ) ; err != nil {
return nil , err
}
result = append ( result , [ ] any { uuid , belongTo , data , lastChange } )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) TodoGetList ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , "SELECT [uuid] FROM todo WHERE [belong_to] = ?;" , username )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] string { }
for rows . Next ( ) {
var uuid string
if err := rows . Scan ( & uuid ) ; err != nil {
return nil , err
}
result = append ( result , uuid )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) TodoGetDetail ( ctx context . Context , token , uuid string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
var dUuid , dBelongTo , dData , dLastChange string
err = tx . QueryRowContext ( ctx , "SELECT * FROM todo WHERE [belong_to] = ? AND [uuid] = ?;" , username , uuid ) . Scan ( & dUuid , & dBelongTo , & dData , & dLastChange )
if err != nil {
if err == sql . ErrNoRows {
return nil , nil
}
return nil , err
}
return [ ] any { dUuid , dBelongTo , dData , dLastChange } , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) TodoAdd ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
newUuid := utils . GenerateUUID ( )
newLastUpdate := utils . GenerateUUID ( )
returnedData := [ ] any { newUuid , username , "" , newLastUpdate }
if _ , err := tx . ExecContext ( ctx , "INSERT INTO todo VALUES (?, ?, ?, ?);" , newUuid , username , "" , newLastUpdate ) ; err != nil {
return nil , err
}
return returnedData , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) TodoUpdate ( ctx context . Context , token , uuid , data , lastChange string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
// check valid token
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
// update
newLastChange := utils . GenerateUUID ( )
res , err := tx . ExecContext ( ctx , "UPDATE todo SET [data] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;" , data , newLastChange , uuid , lastChange )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to update due to no matched rows or too much rows." )
}
return newLastChange , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) TodoDelete ( ctx context . Context , token , uuid , lastChange string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
// check valid token
if err := tokenOperCheckValid ( ctx , tx , token ) ; err != nil {
return nil , err
}
// delete
res , err := tx . ExecContext ( ctx , "DELETE FROM todo WHERE [uuid] = ? AND [last_change] = ?;" , uuid , lastChange )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
return true , nil
} )
}
// endregion
// region: Admin Namespace
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) AdminGet ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
isAdmin , err := tokenOperIsAdmin ( ctx , tx , username )
if err != nil {
return nil , err
}
if ! isAdmin {
return nil , fmt . Errorf ( "Permission denied." )
}
rows , err := tx . QueryContext ( ctx , "SELECT [name], [is_admin] FROM user;" )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] [ ] any { }
for rows . Next ( ) {
var name string
var isAdminVal int64
if err := rows . Scan ( & name , & isAdminVal ) ; err != nil {
return nil , err
}
result = append ( result , [ ] any { name , isAdminVal == 1 } )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) AdminAdd ( ctx context . Context , token , username string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
caller , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
isAdmin , err := tokenOperIsAdmin ( ctx , tx , caller )
if err != nil {
return nil , err
}
if ! isAdmin {
return nil , fmt . Errorf ( "Permission denied." )
}
newPassword := utils . ComputePasswordHash ( utils . GenerateUUID ( ) )
if _ , err := tx . ExecContext ( ctx , "INSERT INTO user VALUES (?, ?, ?, ?);" , username , newPassword , 0 , utils . GenerateSalt ( ) ) ; err != nil {
return nil , err
}
return [ ] any { username , false } , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) AdminUpdate ( ctx context . Context , token , username string , opts AdminUpdateOptions ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
caller , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
isAdmin , err := tokenOperIsAdmin ( ctx , tx , caller )
if err != nil {
return nil , err
}
if ! isAdmin {
return nil , fmt . Errorf ( "Permission denied." )
}
// construct data
sqlList := [ ] string { }
args := [ ] any { }
// analyse opt arg
if opts . Password != nil {
sqlList = append ( sqlList , "[password] = ?" )
args = append ( args , utils . ComputePasswordHash ( * opts . Password ) )
}
if opts . IsAdmin != nil {
sqlList = append ( sqlList , "[is_admin] = ?" )
if * opts . IsAdmin {
args = append ( args , 1 )
} else {
args = append ( args , 0 )
}
}
if len ( sqlList ) == 0 {
return nil , fmt . Errorf ( "No fields to update." )
}
// execute
args = append ( args , username )
d . deps . Logger . Debug ( "admin update" , "args" , args )
query := "UPDATE user SET " + strings . Join ( sqlList , ", " ) + " WHERE [name] = ?;"
res , err := tx . ExecContext ( ctx , query , args ... )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to update due to no matched rows or too much rows." )
}
return true , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) AdminDelete ( ctx context . Context , token , username string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
caller , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
isAdmin , err := tokenOperIsAdmin ( ctx , tx , caller )
if err != nil {
return nil , err
}
if ! isAdmin {
return nil , fmt . Errorf ( "Permission denied." )
}
// delete
res , err := tx . ExecContext ( ctx , "DELETE FROM user WHERE [name] = ?;" , username )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
return true , nil
} )
}
// region: Profile Namespace
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) ProfileIsAdmin ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
return tokenOperIsAdmin ( ctx , tx , username )
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) ProfileChangePassword ( ctx context . Context , token , password string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
if _ , err := tx . ExecContext ( ctx , "UPDATE user SET [password] = ? WHERE [name] = ?;" , utils . ComputePasswordHash ( password ) , username ) ; err != nil {
return nil , err
}
return true , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) ProfileGetToken ( ctx context . Context , token string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
rows , err := tx . QueryContext ( ctx , "SELECT * FROM token WHERE [user] = ?;" , username )
if err != nil {
return nil , err
}
defer rows . Close ( )
result := [ ] [ ] any { }
for rows . Next ( ) {
var user , tokenVal , ua , ip string
var expireOn int64
if err := rows . Scan ( & user , & tokenVal , & expireOn , & ua , & ip ) ; err != nil {
return nil , err
}
result = append ( result , [ ] any { user , tokenVal , expireOn , ua , ip } )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return result , nil
} )
}
2026-07-16 20:51:18 +08:00
func ( d * Sqlite3Database ) ProfileDeleteToken ( ctx context . Context , token , deleteToken string ) ( any , error ) {
2026-07-16 20:13:49 +08:00
return d . safeOp ( ctx , func ( ctx context . Context , tx * sql . Tx ) ( any , error ) {
username , err := tokenOperGetUsername ( ctx , tx , token )
if err != nil {
return nil , err
}
// delete
res , err := tx . ExecContext ( ctx , "DELETE FROM token WHERE [user] = ? AND [token] = ?;" , username , deleteToken )
if err != nil {
return nil , err
}
n , err := res . RowsAffected ( )
if err != nil {
return nil , err
}
if n != 1 {
return nil , fmt . Errorf ( "Fail to delete due to no matched rows or too much rows." )
}
return true , nil
} )
}
// endregion