2026-01-26 07:53:35 +02:00

330 lines
8.3 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type HealthResponse struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Service string `json:"service"`
DatabaseName string `json:"database_name,omitempty"`
GatewayURL string `json:"gateway_url,omitempty"`
}
type UsersResponse struct {
Users []User `json:"users"`
Total int `json:"total"`
}
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// In-memory storage (used when DATABASE_NAME is not set)
var users = []User{
{ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
{ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
{ID: 3, Name: "Charlie", Email: "charlie@example.com", CreatedAt: time.Now()},
}
var nextID = 4
// Environment variables
var (
databaseName = os.Getenv("DATABASE_NAME")
gatewayURL = os.Getenv("GATEWAY_URL")
apiKey = os.Getenv("API_KEY")
)
// executeSQL executes a SQL query against the hosted SQLite database
func executeSQL(query string, args ...interface{}) ([]map[string]interface{}, error) {
if databaseName == "" || gatewayURL == "" {
return nil, fmt.Errorf("database not configured")
}
// Build the query with parameters
reqBody := map[string]interface{}{
"sql": query,
"params": args,
}
bodyBytes, _ := json.Marshal(reqBody)
url := fmt.Sprintf("%s/v1/db/%s/query", gatewayURL, databaseName)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("database error: %s", string(body))
}
var result struct {
Rows []map[string]interface{} `json:"rows"`
Columns []string `json:"columns"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Rows, nil
}
// initDatabase creates the users table if it doesn't exist
func initDatabase() error {
if databaseName == "" || gatewayURL == "" {
log.Printf("DATABASE_NAME or GATEWAY_URL not set, using in-memory storage")
return nil
}
query := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
_, err := executeSQL(query)
if err != nil {
// Log but don't fail - the table might already exist
log.Printf("Warning: Could not create users table: %v", err)
} else {
log.Printf("Users table initialized")
}
return nil
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(HealthResponse{
Status: "healthy",
Timestamp: time.Now(),
Service: "go-backend-test",
DatabaseName: databaseName,
GatewayURL: gatewayURL,
})
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Check if database is configured
useDatabase := databaseName != "" && gatewayURL != ""
switch r.Method {
case http.MethodGet:
if useDatabase {
// Query from hosted SQLite
rows, err := executeSQL("SELECT id, name, email, created_at FROM users ORDER BY id")
if err != nil {
log.Printf("Database query error: %v", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
var dbUsers []User
for _, row := range rows {
user := User{
ID: int(row["id"].(float64)),
Name: row["name"].(string),
Email: row["email"].(string),
}
if ct, ok := row["created_at"].(string); ok {
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", ct)
}
dbUsers = append(dbUsers, user)
}
json.NewEncoder(w).Encode(UsersResponse{
Users: dbUsers,
Total: len(dbUsers),
})
} else {
// Use in-memory storage
json.NewEncoder(w).Encode(UsersResponse{
Users: users,
Total: len(users),
})
}
case http.MethodPost:
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Name == "" || req.Email == "" {
http.Error(w, "Name and email are required", http.StatusBadRequest)
return
}
if useDatabase {
// Insert into hosted SQLite
_, err := executeSQL("INSERT INTO users (name, email) VALUES (?, ?)", req.Name, req.Email)
if err != nil {
log.Printf("Database insert error: %v", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
// Get the inserted user (last insert ID)
rows, err := executeSQL("SELECT id, name, email, created_at FROM users WHERE name = ? AND email = ? ORDER BY id DESC LIMIT 1", req.Name, req.Email)
if err != nil || len(rows) == 0 {
http.Error(w, "Failed to retrieve created user", http.StatusInternalServerError)
return
}
newUser := User{
ID: int(rows[0]["id"].(float64)),
Name: rows[0]["name"].(string),
Email: rows[0]["email"].(string),
}
if ct, ok := rows[0]["created_at"].(string); ok {
newUser.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", ct)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"user": newUser,
})
} else {
// Use in-memory storage
newUser := User{
ID: nextID,
Name: req.Name,
Email: req.Email,
CreatedAt: time.Now(),
}
nextID++
users = append(users, newUser)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"user": newUser,
})
}
case http.MethodDelete:
// Parse user ID from query string (e.g., /api/users?id=1)
idStr := r.URL.Query().Get("id")
if idStr == "" {
http.Error(w, "User ID required", http.StatusBadRequest)
return
}
var id int
fmt.Sscanf(idStr, "%d", &id)
if useDatabase {
_, err := executeSQL("DELETE FROM users WHERE id = ?", id)
if err != nil {
log.Printf("Database delete error: %v", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
} else {
// Delete from in-memory storage
for i, u := range users {
if u.ID == id {
users = append(users[:i], users[i+1:]...)
break
}
}
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "User deleted",
})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
storageType := "in-memory"
if databaseName != "" && gatewayURL != "" {
storageType = "hosted-sqlite"
}
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Orama Network Go Backend Test",
"version": "1.0.0",
"storage": storageType,
"endpoints": map[string]string{
"health": "GET /health",
"users": "GET/POST/DELETE /api/users",
},
"config": map[string]string{
"database_name": maskIfSet(databaseName),
"gateway_url": maskIfSet(gatewayURL),
},
})
}
func maskIfSet(s string) string {
if s == "" {
return "[not configured]"
}
if strings.Contains(s, "://") {
// Mask URL partially
return s[:strings.Index(s, "://")+3] + "..."
}
return "[configured]"
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// Initialize database if configured
if err := initDatabase(); err != nil {
log.Printf("Warning: Database initialization failed: %v", err)
}
http.HandleFunc("/", rootHandler)
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/api/users", usersHandler)
log.Printf("Starting Go backend on port %s", port)
log.Printf("Database: %s", maskIfSet(databaseName))
log.Printf("Gateway: %s", maskIfSet(gatewayURL))
log.Fatal(http.ListenAndServe(":"+port, nil))
}