Skip to content

Commit

Permalink
feat: Added PUT and PATCH routes (#38)
Browse files Browse the repository at this point in the history
- Added PUT and PATCH routes
- Improved request error handling
- Reorganized code

Closes #5
  • Loading branch information
deanefrati authored Oct 14, 2024
1 parent 7b584c9 commit 9d91c40
Show file tree
Hide file tree
Showing 19 changed files with 401 additions and 107 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Temporary Items
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
__debug*

# Local History for Visual Studio Code
.history/
Expand Down
18 changes: 8 additions & 10 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import (
"github.com/spf13/viper"
openapigodoc "github.com/tink3rlabs/openapi-godoc"

"todo-service/internal/errors"
"todo-service/internal/health"
"todo-service/internal/leadership"
"todo-service/internal/logger"
"todo-service/internal/middlewares"
"todo-service/internal/storage"
"todo-service/routes"
"todo-service/types"
)

var serverCommand = &cobra.Command{
Expand All @@ -45,7 +46,7 @@ func initRoutes() *chi.Mux {
middleware.Recoverer, // Recover from panics without crashing server
cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
Expand Down Expand Up @@ -179,21 +180,18 @@ func runServer(cmd *cobra.Command, args []string) error {

//health check - readiness
healthChecker := health.NewHealthChecker()
router.Get("/health/readiness", func(w http.ResponseWriter, r *http.Request) {
h := middlewares.ErrorHandler{}
router.Get("/health/readiness", h.Wrap(func(w http.ResponseWriter, r *http.Request) error {
err := healthChecker.Check(viper.GetBool("health.storage"), viper.GetStringSlice("health.dependencies"))
if err != nil {
slog.Error("health check readiness failed", slog.Any("error", err.Error()))
response := types.ErrorResponse{
Status: http.StatusText(http.StatusServiceUnavailable),
Error: err.Error(),
}
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, response)
return &errors.ServiceUnavailable{Message: err.Error()}
} else {
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
return nil
}
})
}))

port := viper.GetString("service.port")
listenAddress := fmt.Sprintf(":%s", port)
Expand Down
3 changes: 2 additions & 1 deletion config/migrations/mysql/01__base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ migrations:
- migrate: >
CREATE TABLE IF NOT EXISTS todos (
id VARCHAR(50) PRIMARY KEY,
summary TEXT
summary TEXT,
done BOOLEAN
)
rollback: DROP TABLE IF EXISTS todos
3 changes: 2 additions & 1 deletion config/migrations/postgresql/01__base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ migrations:
- migrate: >
CREATE TABLE IF NOT EXISTS todos (
id TEXT PRIMARY KEY,
summary TEXT
summary TEXT,
done BOOLEAN
)
rollback: DROP TABLE IF EXISTS todos
3 changes: 2 additions & 1 deletion config/migrations/sqlite/01__base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ migrations:
- migrate: >
CREATE TABLE IF NOT EXISTS todos (
id TEXT PRIMARY KEY,
summary TEXT
summary TEXT,
done INTEGER
)
rollback: DROP TABLE IF EXISTS todos
5 changes: 5 additions & 0 deletions features/todo/todoService.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func (t *TodoService) DeleteTodo(id string) error {
return t.storage.Delete(&types.Todo{}, "Id", id)
}

func (t *TodoService) UpdateTodo(todoToUpdate types.Todo) error {
return t.storage.Update(todoToUpdate, "Id", todoToUpdate.Id)
}

func (t *TodoService) CreateTodo(todoToCreate types.TodoUpdate) (types.Todo, error) {
todo := types.Todo{}

Expand All @@ -66,6 +70,7 @@ func (t *TodoService) CreateTodo(todoToCreate types.TodoUpdate) (types.Todo, err

todo.Id = id.String()
todo.Summary = todoToCreate.Summary
todo.Done = todoToCreate.Done

err = t.storage.Create(todo)
return todo, err
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22.4

require (
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
github.com/evanphx/json-patch/v5 v5.9.0
github.com/getkin/kin-openapi v0.125.0
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
Expand Down Expand Up @@ -35,6 +36,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
)

require (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
Expand Down Expand Up @@ -114,6 +116,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
25 changes: 25 additions & 0 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package errors

type BadRequest struct {
Message string
}

func (e *BadRequest) Error() string {
return e.Message
}

type NotFound struct {
Message string
}

func (e *NotFound) Error() string {
return e.Message
}

type ServiceUnavailable struct {
Message string
}

func (e *ServiceUnavailable) Error() string {
return e.Message
}
64 changes: 64 additions & 0 deletions internal/middlewares/error_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package middlewares

import (
"errors"
"net/http"

"github.com/go-chi/render"

serviceErrors "todo-service/internal/errors"
"todo-service/internal/storage"
"todo-service/internal/types"
)

type ErrorHandler struct{}

func (e *ErrorHandler) Wrap(handler func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var notFoundError *serviceErrors.NotFound
var badRequestError *serviceErrors.BadRequest
var serviceUnavailable *serviceErrors.ServiceUnavailable

err := handler(w, r)

if (errors.As(err, &notFoundError)) || (errors.Is(err, storage.ErrNotFound)) {
render.Status(r, http.StatusNotFound)
response := types.ErrorResponse{
Status: http.StatusText(http.StatusNotFound),
Error: err.Error(),
}
render.JSON(w, r, response)
return
}

if errors.As(err, &badRequestError) {
render.Status(r, http.StatusBadRequest)
response := types.ErrorResponse{
Status: http.StatusText(http.StatusBadRequest),
Error: err.Error(),
}
render.JSON(w, r, response)
return
}

if errors.As(err, &serviceUnavailable) {
render.Status(r, http.StatusServiceUnavailable)
response := types.ErrorResponse{
Status: http.StatusText(http.StatusServiceUnavailable),
Error: err.Error(),
}
render.JSON(w, r, response)
return
}

if err != nil {
render.Status(r, http.StatusInternalServerError)
response := types.ErrorResponse{
Status: http.StatusText(http.StatusInternalServerError),
Error: "Encountered an unexpected server error: " + err.Error(),
}
render.JSON(w, r, response)
return
}
}
}
6 changes: 5 additions & 1 deletion internal/storage/dynamodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (s *DynamoDBAdapter) Create(item any) error {
})

if err != nil {
return fmt.Errorf("failed to create item: %v", err)
return fmt.Errorf("failed to create or update item: %v", err)
}

return nil
Expand Down Expand Up @@ -117,6 +117,10 @@ func (s *DynamoDBAdapter) Get(dest any, itemKey string, itemValue string) error
}
}

func (s *DynamoDBAdapter) Update(item any, itemKey string, itemValue string) error {
return s.Create(item)
}

func (s *DynamoDBAdapter) Delete(item any, itemKey string, itemValue string) error {
key, err := attributevalue.MarshalMap(map[string]string{strings.ToLower(itemKey): itemValue})
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions internal/storage/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ func (m *MemoryAdapter) Get(dest any, itemKey string, itemValue string) error {
return ErrNotFound
}

func (m *MemoryAdapter) Update(item any, itemKey string, itemValue string) error {
t := reflect.TypeOf(item).String()
for i, existingItem := range m.store[t] {
if reflect.ValueOf(existingItem).FieldByName(itemKey).String() == itemValue {
m.store[t][i] = item
return nil
}
}
return ErrNotFound
}

func (m *MemoryAdapter) Delete(item any, itemKey string, itemValue string) error {
t := strings.ReplaceAll(reflect.TypeOf(item).String(), "*", "")
for k, v := range m.store[t] {
Expand Down
5 changes: 5 additions & 0 deletions internal/storage/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ func (s *SQLAdapter) Get(dest any, itemKey string, itemValue string) error {
return result.Error
}

func (s *SQLAdapter) Update(item any, itemKey string, itemValue string) error {
result := s.DB.Where(itemKey+" = ?", itemValue).Save(item)
return result.Error
}

func (s *SQLAdapter) Delete(item any, itemKey string, itemValue string) error {
result := s.DB.Where(itemKey+" = ?", itemValue).Delete(item)
return result.Error
Expand Down
1 change: 1 addition & 0 deletions internal/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type StorageAdapter interface {
Ping() error
Create(item any) error
Get(dest any, itemKey string, itemValue string) error
Update(item any, itemKey string, itemValue string) error
Delete(item any, itemKey string, itemValue string) error
List(items any, itemKey string, limit int, cursor string) (string, error)
}
Expand Down
62 changes: 62 additions & 0 deletions internal/types/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package types

// @openapi
// components:
//
// responses:
// NotFound:
// description: The specified resource was not found
// content:
// application/json:
// schema:
// $ref: '#/components/schemas/Error'
// Unauthorized:
// description: Unauthorized
// content:
// application/json:
// schema:
// $ref: '#/components/schemas/Error'
// schemas:
// Error:
// type: object
// properties:
// status:
// type: string
// error:
// type: string
type ErrorResponse struct {
Status string `json:"status"`
Error string `json:"error"`
}

// @openapi
// components:
//
// schemas:
// PatchBody:
// type: object
// description: A JSONPatch document as defined by RFC 6902
// additionalProperties: false
// required:
// - op
// - path
// properties:
// op:
// type: string
// description: The operation to be performed
// enum:
// - add
// - remove
// - replace
// - move
// - copy
// - test
// path:
// type: string
// description: A JSON-Pointer
// value:
// description: The value to be used within the operations.
// from:
// type: string
// description: A string containing a JSON Pointer value.
type PatchBody struct{}
37 changes: 0 additions & 37 deletions routes/formatter.go

This file was deleted.

Loading

0 comments on commit 9d91c40

Please sign in to comment.