package main import ( "database/sql" "fmt" "os" _ "github.com/go-sql-driver/mysql" ) // DB is the shared database connection pool used by the application. var DB *sql.DB // InitDB opens a connection to the MariaDB/MySQL database using credentials // from the environment, creates the database if it does not exist, and ensures // the required tables are present. func InitDB() (*sql.DB, error) { password := os.Getenv("DB_PASSWORD") if password == "" { password = "secret" } host := os.Getenv("DB_HOST") if host == "" { // Default to the Docker Compose service name. host = "db" } // Build the DSN. The default host "db" matches the service name in docker-compose.yml. dsn := fmt.Sprintf("oworganizer:%s@tcp(%s:3306)/?parseTime=true", password, host) db, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } // Ensure the application database exists. if _, err := db.Exec("CREATE DATABASE IF NOT EXISTS oworganizer CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); err != nil { return nil, fmt.Errorf("failed to create database: %w", err) } // Switch to the application database. if _, err := db.Exec("USE oworganizer"); err != nil { return nil, fmt.Errorf("failed to select database: %w", err) } // Ensure required tables exist. if err := createTables(db); err != nil { return nil, fmt.Errorf("failed to create tables: %w", err) } return db, nil } // tableExists reports whether a table with the given name exists in the current // database. func tableExists(db *sql.DB, name string) (bool, error) { var exists bool query := ` SELECT EXISTS ( SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ) ` err := db.QueryRow(query, name).Scan(&exists) if err != nil { return false, err } return exists, nil } // createTables creates the application's tables if they do not already exist. func createTables(db *sql.DB) error { tables := map[string]string{ "users": ` CREATE TABLE IF NOT EXISTS users ( userId VARCHAR(255) PRIMARY KEY, rootObjectId VARCHAR(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, "objects": ` CREATE TABLE IF NOT EXISTS objects ( objectId VARCHAR(255) PRIMARY KEY, userId VARCHAR(255) NOT NULL, objectData TEXT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, } for name, ddl := range tables { exists, err := tableExists(db, name) if err != nil { return fmt.Errorf("failed to check %s table: %w", name, err) } if exists { continue } if _, err := db.Exec(ddl); err != nil { return fmt.Errorf("failed to create %s table: %w", name, err) } } return nil } // User represents a row in the users table. type User struct { UserID string RootObjectID string } // CreateUser inserts a new user into the users table. func CreateUser(userID, rootObjectID string) error { _, err := DB.Exec( "INSERT INTO users (userId, rootObjectId) VALUES (?, ?)", userID, rootObjectID, ) if err != nil { return fmt.Errorf("failed to create user: %w", err) } return nil } // GetUser retrieves a user by ID. func GetUser(userID string) (User, error) { var user User err := DB.QueryRow( "SELECT userId, rootObjectId FROM users WHERE userId = ?", userID, ).Scan(&user.UserID, &user.RootObjectID) if err != nil { if err == sql.ErrNoRows { return User{}, fmt.Errorf("user not found") } return User{}, fmt.Errorf("failed to get user: %w", err) } return user, nil } // UpdateUser updates the rootObjectId for a user. func UpdateUser(userID, rootObjectID string) error { result, err := DB.Exec( "UPDATE users SET rootObjectId = ? WHERE userId = ?", rootObjectID, userID, ) if err != nil { return fmt.Errorf("failed to update user: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to check update result: %w", err) } if rows == 0 { return fmt.Errorf("user not found") } return nil } // DeleteUser removes a user by ID. func DeleteUser(userID string) error { result, err := DB.Exec("DELETE FROM users WHERE userId = ?", userID) if err != nil { return fmt.Errorf("failed to delete user: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to check delete result: %w", err) } if rows == 0 { return fmt.Errorf("user not found") } return nil } // Object represents a row in the objects table. type Object struct { ObjectID string UserID string ObjectData string } // CreateObject inserts a new object into the objects table. func CreateObject(objectID, userID, objectData string) error { _, err := DB.Exec( "INSERT INTO objects (objectId, userId, objectData) VALUES (?, ?, ?)", objectID, userID, objectData, ) if err != nil { return fmt.Errorf("failed to create object: %w", err) } return nil } // GetObject retrieves an object by ID. func GetObject(objectID string) (Object, error) { var obj Object err := DB.QueryRow( "SELECT objectId, userId, objectData FROM objects WHERE objectId = ?", objectID, ).Scan(&obj.ObjectID, &obj.UserID, &obj.ObjectData) if err != nil { if err == sql.ErrNoRows { return Object{}, fmt.Errorf("object not found") } return Object{}, fmt.Errorf("failed to get object: %w", err) } return obj, nil } // GetObjectsByUser retrieves all objects belonging to a user. func GetObjectsByUser(userID string) ([]Object, error) { rows, err := DB.Query( "SELECT objectId, userId, objectData FROM objects WHERE userId = ?", userID, ) if err != nil { return nil, fmt.Errorf("failed to query objects: %w", err) } defer rows.Close() var objects []Object for rows.Next() { var obj Object if err := rows.Scan(&obj.ObjectID, &obj.UserID, &obj.ObjectData); err != nil { return nil, fmt.Errorf("failed to scan object: %w", err) } objects = append(objects, obj) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("failed to iterate objects: %w", err) } return objects, nil } // UpdateObject updates the objectData for an object. func UpdateObject(objectID, objectData string) error { result, err := DB.Exec( "UPDATE objects SET objectData = ? WHERE objectId = ?", objectData, objectID, ) if err != nil { return fmt.Errorf("failed to update object: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to check update result: %w", err) } if rows == 0 { return fmt.Errorf("object not found") } return nil } // DeleteObject removes an object by ID. func DeleteObject(objectID string) error { result, err := DB.Exec("DELETE FROM objects WHERE objectId = ?", objectID) if err != nil { return fmt.Errorf("failed to delete object: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to check delete result: %w", err) } if rows == 0 { return fmt.Errorf("object not found") } return nil }