From 9ecae23ccb2caf7efc19f2cc0f8b148374fd3528 Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 15:11:30 +0800 Subject: [PATCH 01/10] Create migration script for database table users and tasks --- ...220630010411_create_tables_users_tasks.sql | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 migrations/20220630010411_create_tables_users_tasks.sql diff --git a/migrations/20220630010411_create_tables_users_tasks.sql b/migrations/20220630010411_create_tables_users_tasks.sql new file mode 100644 index 000000000..3fd3136d9 --- /dev/null +++ b/migrations/20220630010411_create_tables_users_tasks.sql @@ -0,0 +1,49 @@ +-- +goose Up +-- +goose StatementBegin +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE users ( + user_id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v1(), + username character varying(200) NOT NULL UNIQUE, + task_daily_limit int NOT NULL DEFAULT 5, + created_by UUID NOT NULL, + updated_by UUID, + created_when timestamptz NOT NULL, + updated_when timestamptz, + is_active boolean NOT NULL DEFAULT true +); + +CREATE TABLE tasks ( + task_id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v1(), + user_id UUID REFERENCES users(user_id) NOT NULL, + title character varying(200) NOT NULL, + description text, + created_by UUID NOT NULL, + updated_by UUID, + created_when timestamptz NOT NULL, + updated_when timestamptz, + is_active boolean NOT NULL DEFAULT true +); + +CREATE INDEX users_username_is_active_idx ON users USING BTREE (username, is_active); + +CREATE INDEX tasks_user_id_created_when_is_active_idx ON tasks USING BTREE (user_id, created_when, is_active); + +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS users_username_password_is_active_idx; + +DROP INDEX IF EXISTS user_settings_user_id_is_active_idx; + +DROP INDEX IF EXISTS tasks_user_id_is_active_created_when_idx; + +DROP TABLE IF EXISTS tasks; + +DROP TABLE IF EXISTS user_settings; + +DROP TABLE IF EXISTS users; + +DROP EXTENSION IF EXISTS "uuid-ossp"; + +-- +goose StatementEnd \ No newline at end of file From e91f00ff4cb4c02e22f94034ac41ea4b6b68f012 Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 15:12:18 +0800 Subject: [PATCH 02/10] Create models for table users and tasks --- models/base.go | 17 +++++++++++++++++ models/task.go | 13 +++++++++++++ models/user.go | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 models/base.go create mode 100644 models/task.go create mode 100644 models/user.go diff --git a/models/base.go b/models/base.go new file mode 100644 index 000000000..1d5f7dad6 --- /dev/null +++ b/models/base.go @@ -0,0 +1,17 @@ +package models + +import ( + "time" + + uuid "github.com/satori/go.uuid" +) + +// Base model for common columns +type Base struct { + UserID uuid.UUID `json:"userId" gorm:"column:user_id;"` + IsActive bool `json:"isActive" gorm:"column:is_active;"` + CreatedBy uuid.UUID `json:"createdBy" gorm:"column:created_by;"` + UpdatedBy *uuid.UUID `json:"updatedBy" gorm:"column:updated_by;"` + CreatedWhen time.Time `json:"createdWhen" gorm:"column:created_when;"` + UpdatedWhen *time.Time `json:"updatedWhen" gorm:"column:updated_when;"` +} diff --git a/models/task.go b/models/task.go new file mode 100644 index 000000000..a4804c00a --- /dev/null +++ b/models/task.go @@ -0,0 +1,13 @@ +package models + +import ( + uuid "github.com/satori/go.uuid" +) + +// Task model for tasks table +type Task struct { + Base + TaskID uuid.UUID `json:"taskId" gorm:"column:task_id;"` + Title string `json:"title" gorm:"column:title;"` + Description *string `json:"description" gorm:"column:description;"` +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 000000000..4fa66e35a --- /dev/null +++ b/models/user.go @@ -0,0 +1,19 @@ +package models + +import ( + "time" + + uuid "github.com/satori/go.uuid" +) + +// User model for users table +type User struct { + UserID uuid.UUID `json:"id" gorm:"column:user_id;primary_key;"` + Username string `json:"username" gorm:"column:username;"` + TaskDailyLimit int32 `json:"taskDailyLimit" gorm:"column:task_daily_limit;"` + IsActive bool `json:"isActive" gorm:"column:is_active;"` + CreatedWhen time.Time `json:"createdWhen" gorm:"column:created_when;"` + UpdatedWhen *time.Time `json:"updatedWhen" gorm:"column:updated_when;"` + CreatedBy uuid.UUID `json:"createdBy" gorm:"column:created_by;"` + UpdatedBy *uuid.UUID `json:"updatedBy" gorm:"column:updated_by;"` +} From 698167db49ffce2c53b27b23a4547bf064dcd599 Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 15:15:40 +0800 Subject: [PATCH 03/10] Add Connect function for db configuration --- db/db.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 db/db.go diff --git a/db/db.go b/db/db.go new file mode 100644 index 000000000..86063b10c --- /dev/null +++ b/db/db.go @@ -0,0 +1,43 @@ +package db + +import ( + "fmt" + "log" + "os" + "time" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// Connect for postgreSQL DB +func Connect() (*gorm.DB, error) { + + dbname := "psql_togo_db" + + host := os.Getenv("DATABASE_HOST") + port := os.Getenv("DATABASE_PORT") + username := os.Getenv("DATABASE_USERNAME") + password := os.Getenv("DATABASE_PASSWORD") + sslmode := os.Getenv("SSL_MODE") + + dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s", host, port, dbname, username, password, sslmode) + + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: logger.Error, + IgnoreRecordNotFoundError: true, + Colorful: true, + }, + ) + + return gorm.Open(postgres.Open(dsn), + &gorm.Config{ + Logger: newLogger, + PrepareStmt: true, + SkipDefaultTransaction: true, + }) +} From 8741b37ffad4a918e9f32c9948ca0a33d1e26cbf Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 15:16:51 +0800 Subject: [PATCH 04/10] Add server configuration --- .gitignore | 5 +- common/environment/environment.go | 14 ++ common/response/response.go | 54 ++++++++ go.mod | 30 ++++ go.sum | 199 +++++++++++++++++++++++++++ models/request.go | 25 ++++ models/response.go | 16 +++ rest/handler.go | 218 ++++++++++++++++++++++++++++++ rest/utils.go | 42 ++++++ server.go | 50 +++++++ 10 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 common/environment/environment.go create mode 100644 common/response/response.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 models/request.go create mode 100644 models/response.go create mode 100644 rest/handler.go create mode 100644 rest/utils.go create mode 100644 server.go diff --git a/.gitignore b/.gitignore index a512c8b38..c0a71ce99 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ out/ -.idea \ No newline at end of file +.idea + +.env +togo \ No newline at end of file diff --git a/common/environment/environment.go b/common/environment/environment.go new file mode 100644 index 000000000..3ec746638 --- /dev/null +++ b/common/environment/environment.go @@ -0,0 +1,14 @@ +package environment + +import ( + "log" + + "github.com/joho/godotenv" +) + +func Load(path string) { + err := godotenv.Load(path) + if err != nil { + log.Fatalf("Error to load file at %s", path) + } +} diff --git a/common/response/response.go b/common/response/response.go new file mode 100644 index 000000000..2c2610bbc --- /dev/null +++ b/common/response/response.go @@ -0,0 +1,54 @@ +package response + +import ( + "encoding/json" + "net/http" + "togo/models" +) + +func HandleStatusOK(w http.ResponseWriter, message interface{}, data interface{}) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(models.SuccessResponse{ + Status: "Success", + Code: http.StatusOK, + Message: message, + Data: data, + }) +} + +func HandleStatusCreated(w http.ResponseWriter, message interface{}, data interface{}) { + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(models.SuccessResponse{ + Status: "Success", + Code: http.StatusCreated, + Message: message, + Data: data, + }) +} + +func HandleStatusBadRequest(w http.ResponseWriter, message interface{}) { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(models.ErrorResponse{ + Status: "Failed", + Code: http.StatusBadRequest, + Message: message, + }) +} + +func HandleStatusInternalServerError(w http.ResponseWriter, message interface{}) { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(models.ErrorResponse{ + Status: "Failed", + Code: http.StatusInternalServerError, + Message: message, + }) +} + +func HandleStatusNotFound(w http.ResponseWriter, message interface{}) { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(models.ErrorResponse{ + Status: "Failed", + Code: http.StatusNotFound, + Message: message, + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..0ea11ded0 --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module togo + +go 1.18 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/joho/godotenv v1.4.0 + github.com/satori/go.uuid v1.2.0 + github.com/stretchr/testify v1.8.0 + gorm.io/driver/postgres v1.3.7 + gorm.io/gorm v1.23.6 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.12.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.11.0 // indirect + github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..1b8aee977 --- /dev/null +++ b/go.sum @@ -0,0 +1,199 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +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.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= +gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0= +gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/models/request.go b/models/request.go new file mode 100644 index 000000000..f30362ceb --- /dev/null +++ b/models/request.go @@ -0,0 +1,25 @@ +package models + +// CreateUserRequest model for create user request +type CreateUserRequest struct { + Username string `json:"username"` + TaskDailyLimit int32 `json:"taskDailyLimit"` +} + +// UpdateUserRequest model for update user request +type UpdateUserRequest struct { + Username string `json:"username"` + TaskDailyLimit int32 `json:"taskDailyLimit"` +} + +// CreateTaskRequest model for create task request +type CreateTaskRequest struct { + Username string `json:"username"` + Title string `json:"title"` + Description *string `json:"description"` +} + +// DeleteUserRequest model for delete user request +type DeleteUserRequest struct { + Username string `json:"username"` +} diff --git a/models/response.go b/models/response.go new file mode 100644 index 000000000..ee0f890a6 --- /dev/null +++ b/models/response.go @@ -0,0 +1,16 @@ +package models + +// ErrorResponse model for error response +type ErrorResponse struct { + Status string `json:"status"` + Code int `json:"code"` + Message interface{} `json:"message"` +} + +// SuccessResponse model for success response +type SuccessResponse struct { + Status string `json:"status"` + Code int `json:"code"` + Message interface{} `json:"message"` + Data interface{} `json:"data"` +} diff --git a/rest/handler.go b/rest/handler.go new file mode 100644 index 000000000..d8c9ae835 --- /dev/null +++ b/rest/handler.go @@ -0,0 +1,218 @@ +package rest + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "time" + "togo/common/response" + "togo/models" + + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +type TogoRestService struct { + DB *gorm.DB +} + +func Handler(db *gorm.DB) TogoRestService { + return TogoRestService{ + DB: db, + } +} + +func (restService TogoRestService) CreateUser(w http.ResponseWriter, r *http.Request) { + + userRequest := models.CreateUserRequest{} + reqBody, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(reqBody, &userRequest) + + errInput := validateUsername(userRequest.Username) + if errInput != nil { + response.HandleStatusBadRequest(w, errInput.Error()) + return + } + + errInput = validateTaskDailyLimit(userRequest.TaskDailyLimit) + if errInput != nil { + response.HandleStatusBadRequest(w, errInput.Error()) + return + } + + user := &models.User{} + newID := uuid.NewV1() + username := trimLowerUsername(userRequest.Username) + now := time.Now().UTC() + + err := restService.DB.First(&user, "is_active = true AND username = ?", username).Error + + if !errors.Is(err, gorm.ErrRecordNotFound) { + response.HandleStatusBadRequest(w, "user already exist") + return + } + + user = &models.User{ + UserID: newID, + Username: username, + TaskDailyLimit: userRequest.TaskDailyLimit, + IsActive: true, + CreatedWhen: now, + CreatedBy: newID, + } + + err = restService.DB.Create(&user).Error + + if err != nil { + response.HandleStatusInternalServerError(w, err.Error()) + return + } + + response.HandleStatusCreated(w, "user has been successfully created", user) +} + +func (restService TogoRestService) UpdateUser(w http.ResponseWriter, r *http.Request) { + userRequest := models.UpdateUserRequest{} + reqBody, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(reqBody, &userRequest) + + errInput := validateUsername(userRequest.Username) + if errInput != nil { + response.HandleStatusBadRequest(w, errInput.Error()) + return + } + + errInput = validateTaskDailyLimit(userRequest.TaskDailyLimit) + if errInput != nil { + response.HandleStatusBadRequest(w, errInput.Error()) + return + } + + username := trimLowerUsername(userRequest.Username) + + user := &models.User{} + now := time.Now().UTC() + + err := restService.DB.First(&user, "is_active = true AND username = ?", username).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + response.HandleStatusNotFound(w, "user not found") + return + } else if err != nil { + response.HandleStatusInternalServerError(w, err.Error()) + return + } + + user.TaskDailyLimit = userRequest.TaskDailyLimit + user.UpdatedWhen = &now + user.UpdatedBy = &user.UserID + + err = restService.DB.Save(&user).Error + + if err != nil { + response.HandleStatusInternalServerError(w, err.Error()) + return + } + + response.HandleStatusOK(w, "user has been successfully updated", user) +} + +func (restService TogoRestService) CreateTask(w http.ResponseWriter, r *http.Request) { + taskRequest := models.CreateTaskRequest{} + reqBody, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(reqBody, &taskRequest) + var count int64 + now := time.Now().UTC() + + errInput := validateUsername(taskRequest.Username) + if errInput != nil { + response.HandleStatusBadRequest(w, errInput.Error()) + return + } + + errInput = validateTaskTitle(taskRequest.Title) + if errInput != nil { + response.HandleStatusBadRequest(w, errInput.Error()) + return + } + + username := trimLowerUsername(taskRequest.Username) + user := &models.User{} + err := restService.DB.First(&user, "is_active = true AND username = ?", username).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + response.HandleStatusNotFound(w, "user not found") + return + } else if err != nil { + response.HandleStatusInternalServerError(w, err.Error()) + return + } + + today := getBeginningOfDay(time.Now()) + err = restService.DB.Model(&models.Task{}). + Where("is_active = true AND user_id = ? AND created_when > ?", user.UserID, today). + Count(&count).Error + + newID := uuid.NewV1() + + if count >= int64(user.TaskDailyLimit) { + response.HandleStatusBadRequest(w, "user meet the maximum daily limit") + return + } + + task := models.Task{} + task.TaskID = newID + task.Title = taskRequest.Title + task.Description = taskRequest.Description + + task.UserID = user.UserID + task.IsActive = true + task.CreatedBy = user.UserID + task.CreatedWhen = now + + err = restService.DB.Create(&task).Error + + if err != nil { + response.HandleStatusInternalServerError(w, err.Error()) + return + } + + response.HandleStatusCreated(w, "task has been successfully created", task) +} + +func (restService TogoRestService) DeleteUser(w http.ResponseWriter, r *http.Request) { + + userRequest := models.DeleteUserRequest{} + reqBody, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(reqBody, &userRequest) + + errInput := validateUsername(userRequest.Username) + + if errInput != nil { + response.HandleStatusBadRequest(w, errInput.Error()) + return + } + + username := trimLowerUsername(userRequest.Username) + + user := &models.User{} + tasks := &[]models.Task{} + + db := restService.DB.First(&user, "is_active = true AND LOWER(username) = ?", username) + + if db.Error != nil { + response.HandleStatusBadRequest(w, db.Error.Error()) + return + } + + err := restService.DB.Where("is_active = true AND user_id = ?", user.UserID).Delete(&tasks).Error + err = db.Delete(&user, "is_active = true AND user_id = ?", user.UserID).Error + + if err != nil { + response.HandleStatusBadRequest(w, err.Error()) + return + } + + response.HandleStatusOK(w, "user and it's tasks has been successfully deleted", user) +} diff --git a/rest/utils.go b/rest/utils.go new file mode 100644 index 000000000..a0798c011 --- /dev/null +++ b/rest/utils.go @@ -0,0 +1,42 @@ +package rest + +import ( + "errors" + "strings" + "time" +) + +func getBeginningOfDay(t time.Time) time.Time { + year, month, day := t.Date() + return time.Date(year, month, day, 0, 0, 0, 0, t.Location()) +} + +func validateUsername(username string) error { + if trimLowerUsername(username) == "" { + return errors.New("username is required") + } else if len(trimLowerUsername(username)) > 200 { + return errors.New("username has 200 character limit") + } + + return nil +} + +func validateTaskDailyLimit(limit int32) error { + if limit < 1 { + return errors.New("taskDailyLimit must be atleast 1") + } + return nil +} + +func validateTaskTitle(title string) error { + if title == "" { + return errors.New("username is required") + } else if len(title) > 200 { + return errors.New("title has 200 character limit") + } + return nil +} + +func trimLowerUsername(username string) string { + return strings.TrimSpace(strings.ToLower(username)) +} diff --git a/server.go b/server.go new file mode 100644 index 000000000..fe0e87597 --- /dev/null +++ b/server.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "time" + "togo/common/environment" + "togo/db" + "togo/rest" + + "github.com/gorilla/mux" +) + +const defaultPort = "8080" + +func main() { + environment.Load(".env") + + db, err := db.Connect() + + if err != nil { + panic(err) + } + + sqlDB, _ := db.DB() + sqlDB.SetConnMaxLifetime(time.Minute * 30) + sqlDB.SetMaxIdleConns(125) + sqlDB.SetMaxOpenConns(250) + + defer sqlDB.Close() + + port := os.Getenv("PORT") + if port == "" { + port = defaultPort + } + + restService := rest.Handler(db) + + router := mux.NewRouter().StrictSlash(true) + router.HandleFunc("/api/user", restService.CreateUser).Methods("POST") + router.HandleFunc("/api/user", restService.UpdateUser).Methods("PATCH") + router.HandleFunc("/api/user", restService.DeleteUser).Methods("DELETE") + router.HandleFunc("/api/task", restService.CreateTask).Methods("POST") + + log.Printf("Server running on http://localhost:%s", port) + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), router)) +} From 9f357ce7167cd49eaf1fe6d382377b36f20a337b Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 15:17:18 +0800 Subject: [PATCH 05/10] Add go test files for rest directory --- rest/handler_test.go | 113 +++++++++++++++++++++++++++++++++++++++++++ rest/utils_test.go | 60 +++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 rest/handler_test.go create mode 100644 rest/utils_test.go diff --git a/rest/handler_test.go b/rest/handler_test.go new file mode 100644 index 000000000..526783b9f --- /dev/null +++ b/rest/handler_test.go @@ -0,0 +1,113 @@ +package rest + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "togo/common/environment" + "togo/db" + "togo/models" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +const username = "test_user" + +func Router() (*mux.Router, error) { + environment.Load("../.env") + db, err := db.Connect() + restService := Handler(db) + + router := mux.NewRouter() + router.HandleFunc("/api/user", restService.CreateUser).Methods("POST") + router.HandleFunc("/api/user", restService.UpdateUser).Methods("PATCH") + router.HandleFunc("/api/user", restService.DeleteUser).Methods("DELETE") + router.HandleFunc("/api/task", restService.CreateTask).Methods("POST") + + return router, err +} + +func TestCreateUser(t *testing.T) { + + requestBody, _ := json.Marshal(models.CreateUserRequest{ + Username: username, + TaskDailyLimit: 2, + }) + + request, _ := http.NewRequest("POST", "/api/user", bytes.NewBuffer(requestBody)) + + response := httptest.NewRecorder() + + router, errRouter := Router() + + assert.Nil(t, errRouter, "The `errRouter` should be nil") + + router.ServeHTTP(response, request) + + assert.Equal(t, 201, response.Code, "201 Created is expected") +} + +func TestUpdateUser(t *testing.T) { + + requestBody, _ := json.Marshal(models.CreateUserRequest{ + Username: username, + TaskDailyLimit: 1, + }) + + request, _ := http.NewRequest("PATCH", "/api/user", bytes.NewBuffer(requestBody)) + + response := httptest.NewRecorder() + + router, errRouter := Router() + + assert.Nil(t, errRouter, "The `errRouter` should be nil") + + router.ServeHTTP(response, request) + + assert.Equal(t, 200, response.Code, "200 OK is expected") +} + +func TestCreateTask(t *testing.T) { + + description := "Sample description" + + requestBody, _ := json.Marshal(models.CreateTaskRequest{ + Username: username, + Title: "Sample title", + Description: &description, + }) + + request, _ := http.NewRequest("POST", "/api/task", bytes.NewBuffer(requestBody)) + + response := httptest.NewRecorder() + + router, errRouter := Router() + + assert.Nil(t, errRouter, "The `errRouter` should not be nil") + + router.ServeHTTP(response, request) + + assert.Equal(t, 201, response.Code, "201 Created is expected") +} + +func TestDeleteUser(t *testing.T) { + + requestBody, _ := json.Marshal(models.DeleteUserRequest{ + Username: username, + }) + + request, _ := http.NewRequest("DELETE", "/api/user", bytes.NewBuffer(requestBody)) + + response := httptest.NewRecorder() + + router, errRouter := Router() + + assert.Nil(t, errRouter, "The `errRouter` should not be nil") + + router.ServeHTTP(response, request) + + assert.Equal(t, 200, response.Code, "200 OK is expected") +} diff --git a/rest/utils_test.go b/rest/utils_test.go new file mode 100644 index 000000000..d285fe18a --- /dev/null +++ b/rest/utils_test.go @@ -0,0 +1,60 @@ +package rest + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const characters201 = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis." + +func TestGetBeginningOfDay(t *testing.T) { + date := getBeginningOfDay(time.Now()) + assert.NotNil(t, date, "The `date` should not be nil") + assert.Equal(t, "Local", date.Location().String(), "The `date.Location()` should be UTC") +} +func TestValidateUsername(t *testing.T) { + err := validateUsername("mariiia") + + assert.Nil(t, err, "The `err` should be nil") + + err = validateUsername("") + + assert.NotNil(t, err, "The `err` should not be nil") + + err = validateUsername(characters201) + + assert.NotNil(t, err, "The `err` should not be nil") +} + +func TestValidateTaskDailyLimit(t *testing.T) { + err := validateTaskDailyLimit(1) + + assert.Nil(t, err, "The `err` should be nil") + + err = validateTaskDailyLimit(0) + + assert.NotNil(t, err, "The `err` should not be nil") +} + +func TestValidateTaskTitle(t *testing.T) { + err := validateTaskTitle("Sample title") + + assert.Nil(t, err, "The `err` should be nil") + + err = validateTaskTitle("") + + assert.NotNil(t, err, "The `err` should not be nil") + + err = validateTaskTitle(characters201) + + assert.NotNil(t, err, "The `err` should not be nil") + +} + +func TestTrimLowerUsername(t *testing.T) { + username := trimLowerUsername(" Mariiia ") + + assert.Equal(t, "mariiia", username, "The `username` should not be `mariiia`") +} From f02cc311364e1f4da581e436feddde27d8e32d15 Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 15:17:38 +0800 Subject: [PATCH 06/10] Update README --- README.md | 143 ++++++++++++++++++++++++++++++++++++++++-------- Requirements.md | 30 ++++++++++ 2 files changed, 151 insertions(+), 22 deletions(-) create mode 100644 Requirements.md diff --git a/README.md b/README.md index 8df9d4d3a..b14d28df5 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,129 @@ -### Requirements +# A. How to run this code locally? -- Implement one single API which accepts a todo task and records it - - There is a maximum **limit of N tasks per user** that can be added **per day**. - - Different users can have **different** maximum daily limit. -- Write integration (functional) tests -- Write unit tests -- Choose a suitable architecture to make your code simple, organizable, and maintainable -- Write a concise README - - How to run your code locally? - - A sample “curl” command to call your API - - How to run your unit tests locally? - - What do you love about your solution? - - What else do you want us to know about however you do not have enough time to complete? +## Install Go -### Notes +Follow this link to install golang https://golang.org/doc/install -- We're using Golang at Manabie. **However**, we encourage you to use the programming language that you are most comfortable with because we want you to **shine** with all your skills and knowledge. +## Setup Golang $GOPATH directory -### How to submit your solution? +Follow this link to setup $GOPATH env https://golang.org/doc/gopath_code.html -- Fork this repo and show us your development progress via a PR +## Clone this repository in $GOPATH/src directory -### Interesting facts about Manabie +1. Navigate to gopath directory by executing command -- Monthly there are about 2 million lines of code changes (inserted/updated/deleted) committed into our GitHub repositories. To avoid **regression bugs**, we write different kinds of **automated tests** (unit/integration (functionality)/end2end) as parts of the definition of done of our assigned tasks. -- We nurture the cultural values: **knowledge sharing** and **good communication**, therefore good written documents and readable, organizable, and maintainable code are in our blood when we build any features to grow our products. -- We have **collaborative** culture at Manabie. Feel free to ask trieu@manabie.com any questions. We are very happy to answer all of them. +``` +$ cd $GOPATH/src +``` -Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission. +2. Clone the repository by executing command + +```bash +$ git clone [this repository] +``` + +## Create `.env` file from the root folder with variables: + +``` +DATABASE_HOST= +DATABASE_PORT=5432 +DATABASE_USERNAME= +DATABASE_PASSWORD= +SSL_MODE= +``` + +## Install the database migration tool, [goose](https://github.com/pressly/goose) locally + +```bash +$ go install github.com/pressly/goose/v3/cmd/goose@latest +``` + +For macOS users goose is available as a Homebrew Formulae: + +```bash +$ brew install goose +``` + +## Run the database migration scripts + +1. Create a database in your server with the name `psql_togo_db` + +2. Navigate to the `migrations` directory + +```bash +$ cd $GOPATH/src/togo/migrations +``` + +3. Check the migration script status + +```bash +$ goose postgres "user=[your user] password=[your password] dbname=psql_togo_db host=[your host] sslmode=[your sslmode]" status +``` + +4. Run the migration sript to apply the changes in the database + +```bash +$ goose postgres "user=[your user] password=[your password] dbname=psql_togo_db host=[your host] sslmode=[your sslmode]" up +``` + +## Run the API + +### From the root directory `$GOPATH/src/togo/`, run without creating executable + +```bash +$ cd $GOPATH/src/togo/ +$ go run server.go +``` + +### Create executable file + +1. Run the command `go build` to create an executable file +2. Execute the generate file by specifying the name e.g. `./togo` + +# B. Sample “curl” command to call the API + +1. Create User + +```bash +curl -X POST http://localhost:8080/api/user -d '{"username":"readme","taskDailyLimit":2}' +``` + +2. Update User Task Daily Limit + +```bash +curl -X PATCH http://localhost:8080/api/user -d '{"username":"readme","taskDailyLimit":1}' +``` + +3. Create Task + +```bash +curl -X POST http://localhost:8080/api/task -d '{"username":"readme","title":"Sample title","description":"Sample description"}' +``` + +4. Delete User And Created Tasks + +```bash +curl -X DELETE http://localhost:8080/api/user -d '{"username":"readme"}' +``` + +# C. How to run your unit tests locally? + +1. Navigate to `rest` directory + +```bash +$ cd $GOPATH/src/togo/rest +``` + +2. Run the command to execute the test + +```bash +$ go test -v +``` + +# D. What do I love about my solution? + +Deciding to make simple APIs that enabled us to create a user, update a user daily task limit, create task and, delete a user that completes the cycle of integration testing based on the requirements. + +# E. What else do you want us to know about however you do not have enough time to complete? + +I can also create a GraphQL API endpoint with schema first approach written in Golang diff --git a/Requirements.md b/Requirements.md new file mode 100644 index 000000000..8df9d4d3a --- /dev/null +++ b/Requirements.md @@ -0,0 +1,30 @@ +### Requirements + +- Implement one single API which accepts a todo task and records it + - There is a maximum **limit of N tasks per user** that can be added **per day**. + - Different users can have **different** maximum daily limit. +- Write integration (functional) tests +- Write unit tests +- Choose a suitable architecture to make your code simple, organizable, and maintainable +- Write a concise README + - How to run your code locally? + - A sample “curl” command to call your API + - How to run your unit tests locally? + - What do you love about your solution? + - What else do you want us to know about however you do not have enough time to complete? + +### Notes + +- We're using Golang at Manabie. **However**, we encourage you to use the programming language that you are most comfortable with because we want you to **shine** with all your skills and knowledge. + +### How to submit your solution? + +- Fork this repo and show us your development progress via a PR + +### Interesting facts about Manabie + +- Monthly there are about 2 million lines of code changes (inserted/updated/deleted) committed into our GitHub repositories. To avoid **regression bugs**, we write different kinds of **automated tests** (unit/integration (functionality)/end2end) as parts of the definition of done of our assigned tasks. +- We nurture the cultural values: **knowledge sharing** and **good communication**, therefore good written documents and readable, organizable, and maintainable code are in our blood when we build any features to grow our products. +- We have **collaborative** culture at Manabie. Feel free to ask trieu@manabie.com any questions. We are very happy to answer all of them. + +Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission. From bed0da553f239f2e293b43ab80a41037626064a2 Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 16:40:56 +0800 Subject: [PATCH 07/10] Add comments to explain the solution --- common/environment/environment.go | 1 + common/response/response.go | 5 +++ models/task.go | 2 +- models/user.go | 2 +- rest/handler.go | 53 ++++++++++++++++++++++++++----- rest/utils.go | 4 +++ 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/common/environment/environment.go b/common/environment/environment.go index 3ec746638..21a28bbeb 100644 --- a/common/environment/environment.go +++ b/common/environment/environment.go @@ -6,6 +6,7 @@ import ( "github.com/joho/godotenv" ) +// Load for loading the .env file from dynamic path func Load(path string) { err := godotenv.Load(path) if err != nil { diff --git a/common/response/response.go b/common/response/response.go index 2c2610bbc..7342e59a6 100644 --- a/common/response/response.go +++ b/common/response/response.go @@ -6,6 +6,7 @@ import ( "togo/models" ) +// HandleStatusOK for status ok response func HandleStatusOK(w http.ResponseWriter, message interface{}, data interface{}) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(models.SuccessResponse{ @@ -16,6 +17,7 @@ func HandleStatusOK(w http.ResponseWriter, message interface{}, data interface{} }) } +// HandleStatusCreated for status created response func HandleStatusCreated(w http.ResponseWriter, message interface{}, data interface{}) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(models.SuccessResponse{ @@ -26,6 +28,7 @@ func HandleStatusCreated(w http.ResponseWriter, message interface{}, data interf }) } +// HandleStatusBadRequest for status bad request response func HandleStatusBadRequest(w http.ResponseWriter, message interface{}) { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(models.ErrorResponse{ @@ -35,6 +38,7 @@ func HandleStatusBadRequest(w http.ResponseWriter, message interface{}) { }) } +// HandleStatusInternalServerError for status internal server error response func HandleStatusInternalServerError(w http.ResponseWriter, message interface{}) { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(models.ErrorResponse{ @@ -44,6 +48,7 @@ func HandleStatusInternalServerError(w http.ResponseWriter, message interface{}) }) } +// HandleStatusNotFound for not found error response func HandleStatusNotFound(w http.ResponseWriter, message interface{}) { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(models.ErrorResponse{ diff --git a/models/task.go b/models/task.go index a4804c00a..4ccc343d7 100644 --- a/models/task.go +++ b/models/task.go @@ -4,7 +4,7 @@ import ( uuid "github.com/satori/go.uuid" ) -// Task model for tasks table +// Task model for `tasks` table type Task struct { Base TaskID uuid.UUID `json:"taskId" gorm:"column:task_id;"` diff --git a/models/user.go b/models/user.go index 4fa66e35a..b6b4a80f2 100644 --- a/models/user.go +++ b/models/user.go @@ -6,7 +6,7 @@ import ( uuid "github.com/satori/go.uuid" ) -// User model for users table +// User model for `users` table type User struct { UserID uuid.UUID `json:"id" gorm:"column:user_id;primary_key;"` Username string `json:"username" gorm:"column:username;"` diff --git a/rest/handler.go b/rest/handler.go index d8c9ae835..c69bb16d8 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -23,18 +23,22 @@ func Handler(db *gorm.DB) TogoRestService { } } +// CreateUser is the handler for POST /api/user +// This is neccessary to record username and it's daily task limit func (restService TogoRestService) CreateUser(w http.ResponseWriter, r *http.Request) { userRequest := models.CreateUserRequest{} reqBody, _ := ioutil.ReadAll(r.Body) json.Unmarshal(reqBody, &userRequest) + // I add validation for username since it's a required string input and based on the database schema errInput := validateUsername(userRequest.Username) if errInput != nil { response.HandleStatusBadRequest(w, errInput.Error()) return } + // I add validation for taskDailyLimit because it's a number input errInput = validateTaskDailyLimit(userRequest.TaskDailyLimit) if errInput != nil { response.HandleStatusBadRequest(w, errInput.Error()) @@ -46,8 +50,9 @@ func (restService TogoRestService) CreateUser(w http.ResponseWriter, r *http.Req username := trimLowerUsername(userRequest.Username) now := time.Now().UTC() - err := restService.DB.First(&user, "is_active = true AND username = ?", username).Error + err := restService.DB.First(&user, "is_active = true AND LOWER(username) = ?", username).Error + // I add validation if the user is already existing even the users.username has a unique constraint if !errors.Is(err, gorm.ErrRecordNotFound) { response.HandleStatusBadRequest(w, "user already exist") return @@ -62,27 +67,34 @@ func (restService TogoRestService) CreateUser(w http.ResponseWriter, r *http.Req CreatedBy: newID, } + // Insert the user to the `users` table err = restService.DB.Create(&user).Error + // I return error if the database failed to insert the user if err != nil { response.HandleStatusInternalServerError(w, err.Error()) return } + // I return status created when the insert is success response.HandleStatusCreated(w, "user has been successfully created", user) } +// UpdateUser is the handler for PATCH /api/user (optional to call) +// This enable the user to update the task daily limit differently func (restService TogoRestService) UpdateUser(w http.ResponseWriter, r *http.Request) { userRequest := models.UpdateUserRequest{} reqBody, _ := ioutil.ReadAll(r.Body) json.Unmarshal(reqBody, &userRequest) + // I add validation for username since it's a required string input and based on the database schema errInput := validateUsername(userRequest.Username) if errInput != nil { response.HandleStatusBadRequest(w, errInput.Error()) return } + // I add validation for taskDailyLimit because it's a number input errInput = validateTaskDailyLimit(userRequest.TaskDailyLimit) if errInput != nil { response.HandleStatusBadRequest(w, errInput.Error()) @@ -94,12 +106,14 @@ func (restService TogoRestService) UpdateUser(w http.ResponseWriter, r *http.Req user := &models.User{} now := time.Now().UTC() - err := restService.DB.First(&user, "is_active = true AND username = ?", username).Error + err := restService.DB.First(&user, "is_active = true AND LOWER(username) = ?", username).Error + // I return error if the user is not existing in users table if errors.Is(err, gorm.ErrRecordNotFound) { response.HandleStatusNotFound(w, "user not found") return } else if err != nil { + // I return error if the database failed to find the user response.HandleStatusInternalServerError(w, err.Error()) return } @@ -108,59 +122,72 @@ func (restService TogoRestService) UpdateUser(w http.ResponseWriter, r *http.Req user.UpdatedWhen = &now user.UpdatedBy = &user.UserID + // I update the user to the `users` table err = restService.DB.Save(&user).Error + // I return error if the database failed to update the user if err != nil { response.HandleStatusInternalServerError(w, err.Error()) return } + // I return status ok when the update is success response.HandleStatusOK(w, "user has been successfully updated", user) } +// CreateTask is the handler for POST /api/task +// This enable the user to create task func (restService TogoRestService) CreateTask(w http.ResponseWriter, r *http.Request) { taskRequest := models.CreateTaskRequest{} reqBody, _ := ioutil.ReadAll(r.Body) json.Unmarshal(reqBody, &taskRequest) + var count int64 now := time.Now().UTC() + // I add validation for username since it's a required string input and based on the database schema errInput := validateUsername(taskRequest.Username) if errInput != nil { response.HandleStatusBadRequest(w, errInput.Error()) return } + // I add validation for title since it's a required string input and based on the database schema errInput = validateTaskTitle(taskRequest.Title) if errInput != nil { response.HandleStatusBadRequest(w, errInput.Error()) return } + // There's no validation for description since it's nullable and no character limit in the database schema + username := trimLowerUsername(taskRequest.Username) user := &models.User{} - err := restService.DB.First(&user, "is_active = true AND username = ?", username).Error + err := restService.DB.First(&user, "is_active = true AND LOWER(username) = ?", username).Error + // I return error when the user is not existing in `users` table because the `tasks`` table has relationship to `users` table if errors.Is(err, gorm.ErrRecordNotFound) { response.HandleStatusNotFound(w, "user not found") return } else if err != nil { + // I return error if the database fail to find in users table response.HandleStatusInternalServerError(w, err.Error()) return } + // I get the datetime of the begining of the day in Local time intentionally and use it to get the count of the tasks today := getBeginningOfDay(time.Now()) err = restService.DB.Model(&models.Task{}). - Where("is_active = true AND user_id = ? AND created_when > ?", user.UserID, today). + Where("is_active = true AND user_id = ? AND created_when >= ?", user.UserID, today). Count(&count).Error - newID := uuid.NewV1() - + // I return error if the user meet the maximum daily limit if count >= int64(user.TaskDailyLimit) { response.HandleStatusBadRequest(w, "user meet the maximum daily limit") return } + newID := uuid.NewV1() task := models.Task{} task.TaskID = newID task.Title = taskRequest.Title @@ -171,24 +198,29 @@ func (restService TogoRestService) CreateTask(w http.ResponseWriter, r *http.Req task.CreatedBy = user.UserID task.CreatedWhen = now + // Else, I insert the task in the `tasks` table err = restService.DB.Create(&task).Error + // I return error if the database fail to insert the task if err != nil { response.HandleStatusInternalServerError(w, err.Error()) return } + // I return success when the task has been successfully inserted response.HandleStatusCreated(w, "task has been successfully created", task) } +// DeleteUser is the handler for DELETE /api/user (optional to call) +// I created this for the clean up of integration testing to delete the test user and it's created task func (restService TogoRestService) DeleteUser(w http.ResponseWriter, r *http.Request) { userRequest := models.DeleteUserRequest{} reqBody, _ := ioutil.ReadAll(r.Body) json.Unmarshal(reqBody, &userRequest) + // I add validation for username since it's a required string input and based on the database schema errInput := validateUsername(userRequest.Username) - if errInput != nil { response.HandleStatusBadRequest(w, errInput.Error()) return @@ -201,18 +233,23 @@ func (restService TogoRestService) DeleteUser(w http.ResponseWriter, r *http.Req db := restService.DB.First(&user, "is_active = true AND LOWER(username) = ?", username) + // I return error if the user is not existing in `users` table if db.Error != nil { response.HandleStatusBadRequest(w, db.Error.Error()) return } + // I delete the tasks created by the user err := restService.DB.Where("is_active = true AND user_id = ?", user.UserID).Delete(&tasks).Error + // I delete the user in the `users` table err = db.Delete(&user, "is_active = true AND user_id = ?", user.UserID).Error + // I return error if the database failed to delete user or tasks if err != nil { - response.HandleStatusBadRequest(w, err.Error()) + response.HandleStatusInternalServerError(w, err.Error()) return } + // return status ok when the tasks and user has been delete in the table response.HandleStatusOK(w, "user and it's tasks has been successfully deleted", user) } diff --git a/rest/utils.go b/rest/utils.go index a0798c011..1142dc340 100644 --- a/rest/utils.go +++ b/rest/utils.go @@ -11,6 +11,7 @@ func getBeginningOfDay(t time.Time) time.Time { return time.Date(year, month, day, 0, 0, 0, 0, t.Location()) } +// Validate the username if empty string or if it exceed the character limit func validateUsername(username string) error { if trimLowerUsername(username) == "" { return errors.New("username is required") @@ -21,6 +22,7 @@ func validateUsername(username string) error { return nil } +// Validate the taskDailyLimit less than 1 func validateTaskDailyLimit(limit int32) error { if limit < 1 { return errors.New("taskDailyLimit must be atleast 1") @@ -28,6 +30,7 @@ func validateTaskDailyLimit(limit int32) error { return nil } +// Validate the title if empty string or if it exceed the character limit func validateTaskTitle(title string) error { if title == "" { return errors.New("username is required") @@ -37,6 +40,7 @@ func validateTaskTitle(title string) error { return nil } +// Trim and transform to lowercase the username func trimLowerUsername(username string) string { return strings.TrimSpace(strings.ToLower(username)) } From 9bfaa948e53f318fedd8bea52a3a70ec3858a2cb Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 16:41:47 +0800 Subject: [PATCH 08/10] Add more test cases --- rest/handler_test.go | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/rest/handler_test.go b/rest/handler_test.go index 526783b9f..b38b9ada6 100644 --- a/rest/handler_test.go +++ b/rest/handler_test.go @@ -11,10 +11,11 @@ import ( "togo/models" "github.com/gorilla/mux" + uuid "github.com/satori/go.uuid" "github.com/stretchr/testify/assert" ) -const username = "test_user" +var username = uuid.NewV1().String() func Router() (*mux.Router, error) { environment.Load("../.env") @@ -39,15 +40,21 @@ func TestCreateUser(t *testing.T) { request, _ := http.NewRequest("POST", "/api/user", bytes.NewBuffer(requestBody)) - response := httptest.NewRecorder() - router, errRouter := Router() assert.Nil(t, errRouter, "The `errRouter` should be nil") - router.ServeHTTP(response, request) + response1 := httptest.NewRecorder() + + router.ServeHTTP(response1, request) + + assert.Equal(t, 201, response1.Code, "201 Created is expected") - assert.Equal(t, 201, response.Code, "201 Created is expected") + response2 := httptest.NewRecorder() + + router.ServeHTTP(response2, request) + + assert.Equal(t, 400, response2.Code, "400 Bad Request is expected") } func TestUpdateUser(t *testing.T) { @@ -59,12 +66,12 @@ func TestUpdateUser(t *testing.T) { request, _ := http.NewRequest("PATCH", "/api/user", bytes.NewBuffer(requestBody)) - response := httptest.NewRecorder() - router, errRouter := Router() assert.Nil(t, errRouter, "The `errRouter` should be nil") + response := httptest.NewRecorder() + router.ServeHTTP(response, request) assert.Equal(t, 200, response.Code, "200 OK is expected") @@ -82,15 +89,21 @@ func TestCreateTask(t *testing.T) { request, _ := http.NewRequest("POST", "/api/task", bytes.NewBuffer(requestBody)) - response := httptest.NewRecorder() - router, errRouter := Router() assert.Nil(t, errRouter, "The `errRouter` should not be nil") - router.ServeHTTP(response, request) + response1 := httptest.NewRecorder() + + router.ServeHTTP(response1, request) + + assert.Equal(t, 201, response1.Code, "201 Created is expected") + + response2 := httptest.NewRecorder() + + router.ServeHTTP(response2, request) - assert.Equal(t, 201, response.Code, "201 Created is expected") + assert.Equal(t, 400, response2.Code, "400 Bad Request is expected") } func TestDeleteUser(t *testing.T) { From 6a5280cfb7df0ecef96c6e4a96f48b81770bd236 Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 16:42:02 +0800 Subject: [PATCH 09/10] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b14d28df5..0316866d6 100644 --- a/README.md +++ b/README.md @@ -126,4 +126,5 @@ Deciding to make simple APIs that enabled us to create a user, update a user dai # E. What else do you want us to know about however you do not have enough time to complete? -I can also create a GraphQL API endpoint with schema first approach written in Golang +- Completing the go testing coverage +- I can also create a GraphQL API endpoint with schema first approach written in Golang From a768a1b2e082806d4c9ba4c2f761e7d3d3899483 Mon Sep 17 00:00:00 2001 From: Maridin San Miguel Date: Sat, 2 Jul 2022 17:27:56 +0800 Subject: [PATCH 10/10] Add technologies that are used --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0316866d6..a0599b662 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +# TOGO + +Backend Engineer Coding Challenge + +- Language: GoLang +- Database: PostgreSQL + # A. How to run this code locally? ## Install Go @@ -122,7 +129,7 @@ $ go test -v # D. What do I love about my solution? -Deciding to make simple APIs that enabled us to create a user, update a user daily task limit, create task and, delete a user that completes the cycle of integration testing based on the requirements. +Deciding to make simple APIs that enabled us to create a user, update a user daily task limit differently, create task and, delete a user that completes the cycle of integration testing based on the requirements. # E. What else do you want us to know about however you do not have enough time to complete?