From fc6c52bf5ab317bc5716c1c11762e74f362e47fd Mon Sep 17 00:00:00 2001 From: Sam Jarvinen Date: Wed, 8 Apr 2026 10:47:46 -0700 Subject: [PATCH] feat: implement SQLite database persistence with GORM Replace in-memory user slice with SQLite database using GORM ORM. - Add gorm.io/gorm and gorm.io/driver/sqlite dependencies - Create database package with InitDB and SeedDB functions - Update UserProfile model with GORM tags (uint primary key) - Add CreateUserRequest/UpdateUserRequest DTOs for safe input binding - Refactor controllers to struct-based UserController with DB injection - Implement DeleteUser endpoint (DELETE /api/v1/users/:id) - Add proper error handling: invalid ID -> 400, not found -> 404 - Use deterministic ordering (ORDER BY id) for list queries - Add 15 unit tests covering all CRUD operations and edge cases Closes #15 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/routes.go | 28 +-- controllers/user_controller.go | 143 +++++++++----- controllers/user_controller_test.go | 288 ++++++++++++++++++++++++++++ database/database.go | 43 +++++ database/database_test.go | 74 +++++++ database/testhelper.go | 40 ++++ go.mod | 5 + go.sum | 10 + main.go | 14 +- models/user.go | 14 +- 10 files changed, 602 insertions(+), 57 deletions(-) create mode 100644 controllers/user_controller_test.go create mode 100644 database/database.go create mode 100644 database/database_test.go create mode 100644 database/testhelper.go diff --git a/api/routes.go b/api/routes.go index 12bdbec..e7f3014 100644 --- a/api/routes.go +++ b/api/routes.go @@ -3,36 +3,42 @@ package api import ( "path/filepath" "runtime" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "userprofile-api/controllers" ) // SetupRouter configures the API routes -func SetupRouter() *gin.Engine { +func SetupRouter(db *gorm.DB) *gin.Engine { router := gin.Default() - + // Get the absolute path to the templates directory _, b, _, _ := runtime.Caller(0) basePath := filepath.Dir(filepath.Dir(b)) templatesPath := filepath.Join(basePath, "templates/*") - + // Setup template rendering router.LoadHTMLGlob(templatesPath) - + + uc := controllers.NewUserController(db) + // Root handler shows a nice HTML table of all users - router.GET("/", controllers.HomePageHandler) - + router.GET("/", uc.HomePageHandler) + // API version group v1 := router.Group("/api/v1") { users := v1.Group("/users") { - users.GET("", controllers.GetUsers) - users.GET("/:id", controllers.GetUser) - users.POST("", controllers.CreateUser) - users.PUT("/:id", controllers.UpdateUser) + users.GET("", uc.GetUsers) + users.GET("/:id", uc.GetUser) + users.POST("", uc.CreateUser) + users.PUT("/:id", uc.UpdateUser) + users.DELETE("/:id", uc.DeleteUser) } } - + return router } diff --git a/controllers/user_controller.go b/controllers/user_controller.go index 7216f0b..5b90db4 100644 --- a/controllers/user_controller.go +++ b/controllers/user_controller.go @@ -3,82 +3,139 @@ package controllers import ( "log" "net/http" + "strconv" "github.com/gin-gonic/gin" + "gorm.io/gorm" + "userprofile-api/models" ) -// Sample user data -var users = []models.UserProfile{ - {ID: "1", FullName: "John Doe", Emoji: "๐Ÿ˜€"}, - {ID: "2", FullName: "Jane Smith", Emoji: "๐Ÿš€"}, - {ID: "3", FullName: "Robert Johnson", Emoji: "๐ŸŽธ"}, +// UserController handles user-related HTTP requests. +type UserController struct { + DB *gorm.DB +} + +// NewUserController creates a new UserController with the given database. +func NewUserController(db *gorm.DB) *UserController { + return &UserController{DB: db} } // HomePageHandler renders a HTML page displaying users in a table -func HomePageHandler(c *gin.Context) { +func (uc *UserController) HomePageHandler(c *gin.Context) { log.Println("GET / endpoint called") + + var users []models.UserProfile + if err := uc.DB.Order("id").Find(&users).Error; err != nil { + c.HTML(http.StatusInternalServerError, "users.html", gin.H{"Users": nil}) + return + } + c.HTML(http.StatusOK, "users.html", gin.H{ "Users": users, }) } // GetUsers returns all users -func GetUsers(c *gin.Context) { +func (uc *UserController) GetUsers(c *gin.Context) { log.Println("GET /api/v1/users endpoint called") + + var users []models.UserProfile + if err := uc.DB.Order("id").Find(&users).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) + return + } + c.JSON(http.StatusOK, users) } // GetUser returns a single user by ID -func GetUser(c *gin.Context) { - id := c.Param("id") - - for _, user := range users { - if user.ID == id { - c.JSON(http.StatusOK, user) - return - } +func (uc *UserController) GetUser(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return } - - c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + + var user models.UserProfile + if err := uc.DB.First(&user, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + c.JSON(http.StatusOK, user) } // CreateUser adds a new user -func CreateUser(c *gin.Context) { - var newUser models.UserProfile - - if err := c.ShouldBindJSON(&newUser); err != nil { +func (uc *UserController) CreateUser(c *gin.Context) { + var req models.CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - - // For simplicity, we're just appending to the slice - // In a real application, you would use a database - users = append(users, newUser) - - c.JSON(http.StatusCreated, newUser) + + user := models.UserProfile{ + FullName: req.FullName, + Emoji: req.Emoji, + } + + if err := uc.DB.Create(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) + return + } + + c.JSON(http.StatusCreated, user) } // UpdateUser updates an existing user -func UpdateUser(c *gin.Context) { - id := c.Param("id") - var updatedUser models.UserProfile - - if err := c.ShouldBindJSON(&updatedUser); err != nil { +func (uc *UserController) UpdateUser(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + var user models.UserProfile + if err := uc.DB.First(&user, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + var req models.UpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - - for i, user := range users { - if user.ID == id { - updatedUser.ID = id // Ensure ID doesn't change - users[i] = updatedUser - c.JSON(http.StatusOK, updatedUser) - return - } + + user.FullName = req.FullName + user.Emoji = req.Emoji + + if err := uc.DB.Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) + return } - - c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + + c.JSON(http.StatusOK, user) } -// DeleteUser removes a user by ID TODO \ No newline at end of file +// DeleteUser removes a user by ID +func (uc *UserController) DeleteUser(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + result := uc.DB.Delete(&models.UserProfile{}, id) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) + return + } + + if result.RowsAffected == 0 { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "User deleted"}) +} \ No newline at end of file diff --git a/controllers/user_controller_test.go b/controllers/user_controller_test.go new file mode 100644 index 0000000..bd04d02 --- /dev/null +++ b/controllers/user_controller_test.go @@ -0,0 +1,288 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + + "userprofile-api/database" + "userprofile-api/models" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func setupTestRouter(t *testing.T) (*gin.Engine, *database.TestDB) { + t.Helper() + + tdb, err := database.NewTestDB() + if err != nil { + t.Fatalf("failed to create test DB: %v", err) + } + + uc := NewUserController(tdb.DB) + + router := gin.New() + router.GET("/api/v1/users", uc.GetUsers) + router.GET("/api/v1/users/:id", uc.GetUser) + router.POST("/api/v1/users", uc.CreateUser) + router.PUT("/api/v1/users/:id", uc.UpdateUser) + router.DELETE("/api/v1/users/:id", uc.DeleteUser) + + return router, tdb +} + +func seedUser(t *testing.T, tdb *database.TestDB, fullName, emoji string) models.UserProfile { + t.Helper() + user := models.UserProfile{FullName: fullName, Emoji: emoji} + if err := tdb.DB.Create(&user).Error; err != nil { + t.Fatalf("failed to seed user: %v", err) + } + return user +} + +func TestGetUsersEmpty(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/users", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var users []models.UserProfile + json.Unmarshal(w.Body.Bytes(), &users) + if len(users) != 0 { + t.Errorf("expected 0 users, got %d", len(users)) + } +} + +func TestGetUsersWithData(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + seedUser(t, tdb, "Alice", "๐Ÿ‘ฉ") + seedUser(t, tdb, "Bob", "๐Ÿ‘จ") + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/users", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var users []models.UserProfile + json.Unmarshal(w.Body.Bytes(), &users) + if len(users) != 2 { + t.Fatalf("expected 2 users, got %d", len(users)) + } + + // Verify ordering by ID + if users[0].FullName != "Alice" || users[1].FullName != "Bob" { + t.Error("users are not returned in ID order") + } +} + +func TestGetUserByID(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + user := seedUser(t, tdb, "Alice", "๐Ÿ‘ฉ") + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/users/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var got models.UserProfile + json.Unmarshal(w.Body.Bytes(), &got) + if got.FullName != user.FullName { + t.Errorf("expected %s, got %s", user.FullName, got.FullName) + } +} + +func TestGetUserNotFound(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/users/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status 404, got %d", w.Code) + } +} + +func TestGetUserInvalidID(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/users/abc", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400, got %d", w.Code) + } +} + +func TestCreateUser(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + body, _ := json.Marshal(models.CreateUserRequest{ + FullName: "Charlie", + Emoji: "๐ŸŽ‰", + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Errorf("expected status 201, got %d", w.Code) + } + + var user models.UserProfile + json.Unmarshal(w.Body.Bytes(), &user) + if user.FullName != "Charlie" { + t.Errorf("expected FullName Charlie, got %s", user.FullName) + } + if user.ID == 0 { + t.Error("expected non-zero ID") + } + + // Verify it persisted + var count int64 + tdb.DB.Model(&models.UserProfile{}).Count(&count) + if count != 1 { + t.Errorf("expected 1 user in DB, got %d", count) + } +} + +func TestCreateUserInvalidBody(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + body := []byte(`{"fullName": ""}`) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400, got %d", w.Code) + } +} + +func TestUpdateUser(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + seedUser(t, tdb, "Alice", "๐Ÿ‘ฉ") + + body, _ := json.Marshal(models.UpdateUserRequest{ + FullName: "Alice Updated", + Emoji: "๐ŸŒŸ", + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/v1/users/1", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var user models.UserProfile + json.Unmarshal(w.Body.Bytes(), &user) + if user.FullName != "Alice Updated" { + t.Errorf("expected Alice Updated, got %s", user.FullName) + } + if user.ID != 1 { + t.Errorf("expected ID 1 to be preserved, got %d", user.ID) + } +} + +func TestUpdateUserNotFound(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + body, _ := json.Marshal(models.UpdateUserRequest{ + FullName: "Ghost", + Emoji: "๐Ÿ‘ป", + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/v1/users/999", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status 404, got %d", w.Code) + } +} + +func TestDeleteUser(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + seedUser(t, tdb, "Alice", "๐Ÿ‘ฉ") + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/v1/users/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + // Verify deletion + var count int64 + tdb.DB.Model(&models.UserProfile{}).Count(&count) + if count != 0 { + t.Errorf("expected 0 users after delete, got %d", count) + } +} + +func TestDeleteUserNotFound(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/v1/users/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status 404, got %d", w.Code) + } +} + +func TestDeleteUserInvalidID(t *testing.T) { + router, tdb := setupTestRouter(t) + defer tdb.Close() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/v1/users/abc", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400, got %d", w.Code) + } +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..c677071 --- /dev/null +++ b/database/database.go @@ -0,0 +1,43 @@ +package database + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "userprofile-api/models" +) + +// InitDB opens a SQLite database and runs migrations. +func InitDB(dbPath string) (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return nil, err + } + + if err := db.AutoMigrate(&models.UserProfile{}); err != nil { + return nil, err + } + + return db, nil +} + +// SeedDB populates the database with default users if the table is empty. +func SeedDB(db *gorm.DB) error { + var count int64 + if err := db.Model(&models.UserProfile{}).Count(&count).Error; err != nil { + return err + } + + if count == 0 { + users := []models.UserProfile{ + {FullName: "John Doe", Emoji: "๐Ÿ˜€"}, + {FullName: "Jane Smith", Emoji: "๐Ÿš€"}, + {FullName: "Robert Johnson", Emoji: "๐ŸŽธ"}, + } + if err := db.Create(&users).Error; err != nil { + return err + } + } + + return nil +} diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 0000000..58165e9 --- /dev/null +++ b/database/database_test.go @@ -0,0 +1,74 @@ +package database + +import ( + "testing" + + "userprofile-api/models" +) + +func setupTestDB(t *testing.T) *TestDB { + t.Helper() + tdb, err := NewTestDB() + if err != nil { + t.Fatalf("failed to initialize test database: %v", err) + } + return tdb +} + +func TestInitDB(t *testing.T) { + tdb := setupTestDB(t) + defer tdb.Close() + + if tdb.DB == nil { + t.Fatal("expected non-nil database") + } + + // Verify the table was created by inserting a record + user := models.UserProfile{FullName: "Test User", Emoji: "๐Ÿงช"} + if err := tdb.DB.Create(&user).Error; err != nil { + t.Fatalf("failed to insert into migrated table: %v", err) + } + + if user.ID == 0 { + t.Error("expected auto-generated ID, got 0") + } +} + +func TestSeedDB(t *testing.T) { + tdb := setupTestDB(t) + defer tdb.Close() + + if err := SeedDB(tdb.DB); err != nil { + t.Fatalf("SeedDB failed: %v", err) + } + + var users []models.UserProfile + tdb.DB.Find(&users) + + if len(users) != 3 { + t.Fatalf("expected 3 seeded users, got %d", len(users)) + } + + if users[0].FullName != "John Doe" { + t.Errorf("expected first user to be John Doe, got %s", users[0].FullName) + } +} + +func TestSeedDBIdempotent(t *testing.T) { + tdb := setupTestDB(t) + defer tdb.Close() + + if err := SeedDB(tdb.DB); err != nil { + t.Fatalf("first SeedDB failed: %v", err) + } + if err := SeedDB(tdb.DB); err != nil { + t.Fatalf("second SeedDB failed: %v", err) + } + + var count int64 + tdb.DB.Model(&models.UserProfile{}).Count(&count) + + if count != 3 { + t.Errorf("expected 3 users after double seed, got %d", count) + } +} diff --git a/database/testhelper.go b/database/testhelper.go new file mode 100644 index 0000000..40d691e --- /dev/null +++ b/database/testhelper.go @@ -0,0 +1,40 @@ +package database + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "userprofile-api/models" +) + +// TestDB wraps a GORM DB for testing with proper cleanup. +type TestDB struct { + DB *gorm.DB +} + +// NewTestDB creates an in-memory SQLite database for testing. +func NewTestDB() (*TestDB, error) { + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + if err != nil { + return nil, err + } + + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + sqlDB.SetMaxOpenConns(1) + + if err := db.AutoMigrate(&models.UserProfile{}); err != nil { + return nil, err + } + + return &TestDB{DB: db}, nil +} + +// Close closes the underlying database connection. +func (tdb *TestDB) Close() { + if sqlDB, err := tdb.DB.DB(); err == nil { + sqlDB.Close() + } +} diff --git a/go.mod b/go.mod index 12fb3df..53ecc57 100644 --- a/go.mod +++ b/go.mod @@ -14,10 +14,13 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -30,4 +33,6 @@ require ( golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlite v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect ) diff --git a/go.sum b/go.sum index 2ce8cf0..c5120e0 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,10 @@ github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -34,6 +38,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -71,5 +77,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index 42e5046..352864d 100644 --- a/main.go +++ b/main.go @@ -4,11 +4,21 @@ import ( "log" "userprofile-api/api" + "userprofile-api/database" ) func main() { - router := api.SetupRouter() - + db, err := database.InitDB("users.db") + if err != nil { + log.Fatal("Failed to initialize database:", err) + } + + if err := database.SeedDB(db); err != nil { + log.Fatal("Failed to seed database:", err) + } + + router := api.SetupRouter(db) + log.Println("Starting server on :8080") router.Run(":8080") } diff --git a/models/user.go b/models/user.go index b4d4453..0a3419b 100644 --- a/models/user.go +++ b/models/user.go @@ -2,7 +2,19 @@ package models // UserProfile represents user profile data type UserProfile struct { - ID string `json:"id"` + ID uint `json:"id" gorm:"primaryKey"` FullName string `json:"fullName"` Emoji string `json:"emoji"` } + +// CreateUserRequest is the input for creating a user +type CreateUserRequest struct { + FullName string `json:"fullName" binding:"required"` + Emoji string `json:"emoji" binding:"required"` +} + +// UpdateUserRequest is the input for updating a user +type UpdateUserRequest struct { + FullName string `json:"fullName" binding:"required"` + Emoji string `json:"emoji" binding:"required"` +}