276 lines
7.0 KiB
Go
276 lines
7.0 KiB
Go
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
|
|
}
|