diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json deleted file mode 100644 index 988037b..0000000 --- a/Godeps/Godeps.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "ImportPath": "github.com/RealImage/QLedger", - "GoVersion": "go1.8", - "GodepVersion": "v79", - "Packages": [ - "./..." - ], - "Deps": [ - { - "ImportPath": "github.com/davecgh/go-spew/spew", - "Comment": "v1.0.0-4-g04cdfd4", - "Rev": "04cdfd42973bb9c8589fd6a731800cf222fde1a9" - }, - { - "ImportPath": "github.com/julienschmidt/httprouter", - "Comment": "v1.1-51-g975b5c4", - "Rev": "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e" - }, - { - "ImportPath": "github.com/lib/pq", - "Comment": "go1.0-cutoff-166-g2704adc", - "Rev": "2704adc878c21e1329f46f6e56a1c387d788ff94" - }, - { - "ImportPath": "github.com/lib/pq/oid", - "Comment": "go1.0-cutoff-166-g2704adc", - "Rev": "2704adc878c21e1329f46f6e56a1c387d788ff94" - }, - { - "ImportPath": "github.com/mattes/migrate", - "Comment": "v3.0.1-17-gc1134be", - "Rev": "c1134be427e4db510f745a5a349c6de03a2360f1" - }, - { - "ImportPath": "github.com/mattes/migrate/database", - "Comment": "v3.0.1-17-gc1134be", - "Rev": "c1134be427e4db510f745a5a349c6de03a2360f1" - }, - { - "ImportPath": "github.com/mattes/migrate/database/postgres", - "Comment": "v3.0.1-17-gc1134be", - "Rev": "c1134be427e4db510f745a5a349c6de03a2360f1" - }, - { - "ImportPath": "github.com/mattes/migrate/source", - "Comment": "v3.0.1-17-gc1134be", - "Rev": "c1134be427e4db510f745a5a349c6de03a2360f1" - }, - { - "ImportPath": "github.com/mattes/migrate/source/file", - "Comment": "v3.0.1-17-gc1134be", - "Rev": "c1134be427e4db510f745a5a349c6de03a2360f1" - }, - { - "ImportPath": "github.com/pkg/errors", - "Comment": "v0.8.0-5-gc605e28", - "Rev": "c605e284fe17294bda444b34710735b29d1a9d90" - }, - { - "ImportPath": "github.com/pmezard/go-difflib/difflib", - "Rev": "d8ed2627bdf02c080bf22230dbb337003b7aba2d" - }, - { - "ImportPath": "github.com/stretchr/testify/assert", - "Comment": "v1.1.4-27-g4d4bfba", - "Rev": "4d4bfba8f1d1027c4fdbe371823030df51419987" - }, - { - "ImportPath": "github.com/stretchr/testify/require", - "Comment": "v1.1.4-27-g4d4bfba", - "Rev": "4d4bfba8f1d1027c4fdbe371823030df51419987" - }, - { - "ImportPath": "github.com/stretchr/testify/suite", - "Comment": "v1.1.4-27-g4d4bfba", - "Rev": "4d4bfba8f1d1027c4fdbe371823030df51419987" - } - ] -} diff --git a/Godeps/Readme b/Godeps/Readme deleted file mode 100644 index 4cdaa53..0000000 --- a/Godeps/Readme +++ /dev/null @@ -1,5 +0,0 @@ -This directory tree is generated automatically by godep. - -Please do not edit. - -See https://github.com/tools/godep for more information. diff --git a/controller/accounts.go b/controller/accounts.go new file mode 100644 index 0000000..77168bc --- /dev/null +++ b/controller/accounts.go @@ -0,0 +1,53 @@ +package controller + +import ( + "fmt" + + "github.com/RealImage/QLedger/errors" + e "github.com/RealImage/QLedger/errors" + "github.com/RealImage/QLedger/models" +) + +func (c *Controller) AddAccount(account *models.Account) error { + isExists, err := c.AccountDB.IsExists(account.ID) + if err != nil { + return fmt.Errorf("%w: error while checking for existing account: %v", errors.ErrInternal, err) + } + if isExists { + return fmt.Errorf("%w: account is conflicting: %s", errors.ErrConflict, account.ID) + } + err = c.AccountDB.CreateAccount(account) + if err != nil { + return fmt.Errorf("%w: error while adding account: %s (%v)", errors.ErrInternal, account.ID, err) + } + return nil +} + +func (c *Controller) GetAccounts(query string) (interface{}, error) { + results, err := c.SearchEngine.Query(query) + if err != nil { + switch err.ErrorCode() { + case "search.query.invalid": + return nil, fmt.Errorf("%w: error while querying :%v", e.ErrBadRequest, err) + default: + return nil, fmt.Errorf("%w: error while querying :%v", e.ErrInternal, err) + } + } + return results, nil +} + +func (c *Controller) UpdateAccount(account *models.Account) error { + isExists, err := c.AccountDB.IsExists(account.ID) + if err != nil { + return fmt.Errorf("%w: error while checking for existing account: %v", e.ErrInternal, err) + } + if !isExists { + return fmt.Errorf("%w: account doesn't exist: %v", e.ErrNotFound, err) + } + + err = c.AccountDB.UpdateAccount(account) + if err != nil { + return fmt.Errorf("%w: error while updating account %s (%v)", e.ErrInternal, account.ID, err) + } + return nil +} diff --git a/controller/interfaces.go b/controller/interfaces.go new file mode 100644 index 0000000..d5665fa --- /dev/null +++ b/controller/interfaces.go @@ -0,0 +1,24 @@ +package controller + +import ( + ledgerError "github.com/RealImage/QLedger/errors" + "github.com/RealImage/QLedger/models" +) + +type SearchEngine interface { + Query(q string) (interface{}, ledgerError.ApplicationError) +} + +type AccountDB interface { + GetByID(id string) (*models.Account, ledgerError.ApplicationError) + IsExists(id string) (bool, ledgerError.ApplicationError) + CreateAccount(account *models.Account) ledgerError.ApplicationError + UpdateAccount(account *models.Account) ledgerError.ApplicationError +} + +type TransactionDB interface { + IsExists(id string) (bool, ledgerError.ApplicationError) + IsConflict(transaction *models.Transaction) (bool, ledgerError.ApplicationError) + Transact(txn *models.Transaction) bool + UpdateTransaction(txn *models.Transaction) ledgerError.ApplicationError +} diff --git a/controller/service.go b/controller/service.go new file mode 100644 index 0000000..a0d4e33 --- /dev/null +++ b/controller/service.go @@ -0,0 +1,11 @@ +package controller + +type Controller struct { + SearchEngine SearchEngine + AccountDB AccountDB + TransactionDB TransactionDB +} + +func NewController(s SearchEngine, a AccountDB, t TransactionDB) *Controller { + return &Controller{SearchEngine: s, AccountDB: a, TransactionDB: t} +} diff --git a/controller/transactions.go b/controller/transactions.go new file mode 100644 index 0000000..2c0b6c8 --- /dev/null +++ b/controller/transactions.go @@ -0,0 +1,62 @@ +package controller + +import ( + "fmt" + + e "github.com/RealImage/QLedger/errors" + "github.com/RealImage/QLedger/models" +) + +func (c *Controller) MakeTransaction(transaction *models.Transaction) (bool, error) { + if !transaction.IsValid() { + return false, fmt.Errorf("%w: transaction is invalid: %s", e.ErrBadRequest, transaction.ID) + } + isExists, err := c.TransactionDB.IsExists(transaction.ID) + if err != nil { + return false, fmt.Errorf("%w: error while checking for existing transaction: %v", e.ErrInternal, err) + } + if !isExists { + done := c.TransactionDB.Transact(transaction) + if !done { + return false, fmt.Errorf("%w: transaction failed: %s", e.ErrInternal, transaction.ID) + } + return false, nil + } + isConflict, err := c.TransactionDB.IsConflict(transaction) + if err != nil { + return false, fmt.Errorf("%w: error while checking for conflicting transaction: %v", e.ErrInternal, err) + } + if isConflict { + // The conflicting transactions are denied + return false, fmt.Errorf("%w: transaction is conflicting: %s", e.ErrConflict, transaction.ID) + } + return true, nil +} + +func (c *Controller) GetTransactions(query string) (interface{}, error) { + results, err := c.SearchEngine.Query(query) + if err != nil { + switch err.ErrorCode() { + case "search.query.invalid": + return nil, fmt.Errorf("%w: error while querying: %v", e.ErrBadRequest, err) + default: + return nil, fmt.Errorf("%w: error while querying: %v", e.ErrInternal, err) + } + } + return results, nil +} + +func (c *Controller) UpdateTransaction(transaction *models.Transaction) error { + isExists, err := c.TransactionDB.IsExists(transaction.ID) + if err != nil { + return fmt.Errorf("%w: error while checking for existing transaction: %v", e.ErrInternal, err) + } + if !isExists { + return fmt.Errorf("%w: transaction doesn't exist: %s", e.ErrNotFound, transaction.ID) + } + err = c.TransactionDB.UpdateTransaction(transaction) + if err != nil { + return fmt.Errorf("%w: error while updating transaction: %s (%v)", e.ErrInternal, transaction.ID, err) + } + return nil +} diff --git a/controllers/accounts.go b/controllers/accounts.go deleted file mode 100644 index cec4c5a..0000000 --- a/controllers/accounts.go +++ /dev/null @@ -1,145 +0,0 @@ -package controllers - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "regexp" - - ledgerContext "github.com/RealImage/QLedger/context" - "github.com/RealImage/QLedger/models" -) - -// GetAccounts returns the list of accounts that matches the search query -func GetAccounts(w http.ResponseWriter, r *http.Request, context *ledgerContext.AppContext) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Println("Error reading payload:", err) - w.WriteHeader(http.StatusBadRequest) - return - } - defer r.Body.Close() - query := string(body) - - engine, aerr := models.NewSearchEngine(context.DB, models.SearchNamespaceAccounts) - if aerr != nil { - log.Println("Error while creating Search Engine:", aerr) - w.WriteHeader(http.StatusInternalServerError) - return - } - results, aerr := engine.Query(query) - if aerr != nil { - log.Println("Error while querying:", aerr) - switch aerr.ErrorCode() { - case "search.query.invalid": - w.WriteHeader(http.StatusBadRequest) - return - default: - w.WriteHeader(http.StatusInternalServerError) - return - } - } - - data, err := json.Marshal(results) - if err != nil { - log.Println("Error while parsing results:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write(data) - return -} - -func unmarshalToAccount(r *http.Request, account *models.Account) error { - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - err = json.Unmarshal(body, account) - if err != nil { - return err - } - var validKey = regexp.MustCompile(`^[a-z_A-Z]+$`) - for key := range account.Data { - if !validKey.MatchString(key) { - return fmt.Errorf("Invalid key in data json: %v", key) - } - } - return nil -} - -// AddAccount creates a new account with the input ID and data -func AddAccount(w http.ResponseWriter, r *http.Request, context *ledgerContext.AppContext) { - account := &models.Account{} - err := unmarshalToAccount(r, account) - if err != nil { - log.Println("Error loading payload:", err) - w.WriteHeader(http.StatusBadRequest) - //TODO Should we return any error message? - return - } - - accountsDB := models.NewAccountDB(context.DB) - // Check if an account with same ID already exists - isExists, err := accountsDB.IsExists(account.ID) - if err != nil { - log.Println("Error while checking for existing account:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - if isExists { - log.Println("Account is conflicting:", account.ID) - w.WriteHeader(http.StatusConflict) - return - } - - // Otherwise, add account - aerr := accountsDB.CreateAccount(account) - if aerr != nil { - log.Printf("Error while adding account: %v (%v)", account.ID, aerr) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusCreated) - return -} - -// UpdateAccount updates data of an account with the input ID -func UpdateAccount(w http.ResponseWriter, r *http.Request, context *ledgerContext.AppContext) { - account := &models.Account{} - err := unmarshalToAccount(r, account) - if err != nil { - log.Println("Error loading payload:", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - accountsDB := models.NewAccountDB(context.DB) - // Check if an account with same ID already exists - isExists, err := accountsDB.IsExists(account.ID) - if err != nil { - log.Println("Error while checking for existing account:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - if !isExists { - log.Println("Account doesn't exist:", account.ID) - w.WriteHeader(http.StatusNotFound) - return - } - - // Otherwise, update account - aerr := accountsDB.UpdateAccount(account) - if aerr != nil { - log.Printf("Error while updating account: %v (%v)", account.ID, aerr) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - return -} diff --git a/controllers/transactions.go b/controllers/transactions.go deleted file mode 100644 index 2d3fb5e..0000000 --- a/controllers/transactions.go +++ /dev/null @@ -1,177 +0,0 @@ -package controllers - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "regexp" - "time" - - ledgerContext "github.com/RealImage/QLedger/context" - "github.com/RealImage/QLedger/models" -) - -func unmarshalToTransaction(r *http.Request, txn *models.Transaction) error { - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - err = json.Unmarshal(body, txn) - if err != nil { - return err - } - var validKey = regexp.MustCompile(`^[a-z_A-Z]+$`) - for key := range txn.Data { - if !validKey.MatchString(key) { - return fmt.Errorf("Invalid key in data json: %v", key) - } - } - // Validate timestamp format if present - if txn.Timestamp != "" { - _, err := time.Parse(models.LedgerTimestampLayout, txn.Timestamp) - if err != nil { - return err - } - } - - return nil -} - -// MakeTransaction creates a new transaction from the request data -func MakeTransaction(w http.ResponseWriter, r *http.Request, context *ledgerContext.AppContext) { - transaction := &models.Transaction{} - err := unmarshalToTransaction(r, transaction) - if err != nil { - log.Println("Error loading payload:", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - // Skip if the transaction is invalid - // by validating the delta values - if !transaction.IsValid() { - log.Println("Transaction is invalid:", transaction.ID) - w.WriteHeader(http.StatusBadRequest) - return - } - - transactionsDB := models.NewTransactionDB(context.DB) - // Check if a transaction with same ID already exists - isExists, err := transactionsDB.IsExists(transaction.ID) - if err != nil { - log.Println("Error while checking for existing transaction:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - if isExists { - // Check if the transaction lines are different - // and conflicts with the existing lines - isConflict, err := transactionsDB.IsConflict(transaction) - if err != nil { - log.Println("Error while checking for conflicting transaction:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - if isConflict { - // The conflicting transactions are denied - log.Println("Transaction is conflicting:", transaction.ID) - w.WriteHeader(http.StatusConflict) - return - } - // Otherwise the transaction is just a duplicate - // The exactly duplicate transactions are ignored - // log.Println("Transaction is duplicate:", transaction.ID) - w.WriteHeader(http.StatusAccepted) - return - } - - // Otherwise, do transaction - done := transactionsDB.Transact(transaction) - if !done { - log.Println("Transaction failed:", transaction.ID) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusCreated) - return -} - -// GetTransactions returns the list of transactions that matches the search query -func GetTransactions(w http.ResponseWriter, r *http.Request, context *ledgerContext.AppContext) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Println("Error reading payload:", err) - w.WriteHeader(http.StatusBadRequest) - return - } - defer r.Body.Close() - - engine, aerr := models.NewSearchEngine(context.DB, models.SearchNamespaceTransactions) - if aerr != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - query := string(body) - - results, aerr := engine.Query(query) - if aerr != nil { - log.Println("Error while querying:", aerr) - switch aerr.ErrorCode() { - case "search.query.invalid": - w.WriteHeader(http.StatusBadRequest) - return - default: - w.WriteHeader(http.StatusInternalServerError) - return - } - } - - data, err := json.Marshal(results) - if err != nil { - log.Println("Error while parsing results:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write(data) - return -} - -// UpdateTransaction updates the data of a transaction with the input ID -func UpdateTransaction(w http.ResponseWriter, r *http.Request, context *ledgerContext.AppContext) { - transaction := &models.Transaction{} - err := unmarshalToTransaction(r, transaction) - if err != nil { - log.Println("Error loading payload:", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - transactionDB := models.NewTransactionDB(context.DB) - // Check if a transaction with same ID already exists - isExists, err := transactionDB.IsExists(transaction.ID) - if err != nil { - log.Println("Error while checking for existing transaction:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - if !isExists { - log.Println("Transaction doesn't exist:", transaction.ID) - w.WriteHeader(http.StatusNotFound) - return - } - - // Otherwise, update transaction - terr := transactionDB.UpdateTransaction(transaction) - if terr != nil { - log.Printf("Error while updating transaction: %v (%v)", transaction.ID, terr) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - return -} diff --git a/errors/errors.go b/errors/errors.go index d371a8e..d133ea5 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -1,6 +1,16 @@ package errors -import "fmt" +import ( + "errors" + "fmt" +) + +var ( + ErrBadRequest = errors.New("bad request") + ErrInternal = errors.New("internal error") + ErrConflict = errors.New("conflict") + ErrNotFound = errors.New("not found") +) // TODO: Do we really need this ApplicationError interface ? // Can we just create named variables of type `error` like ErrorJSONInvalid, ErrorAccountConflict, etc., diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f710b5d --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/RealImage/QLedger + +go 1.13 + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v1.13.1 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21 + github.com/lib/pq v0.0.0-20170324204654-2704adc878c2 + github.com/mattes/migrate v3.0.2-0.20170605201158-c1134be427e4+incompatible + github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.2.2 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ca35ac6 --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +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/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= +github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21 h1:F/iKcka0K2LgnKy/fgSBf235AETtm1n1TvBzqu40LE0= +github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/lib/pq v0.0.0-20170324204654-2704adc878c2 h1:2imqQk4oITdu2OSQcxO1A/yR6vftxSUI8iCQo8Ra4Jw= +github.com/lib/pq v0.0.0-20170324204654-2704adc878c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattes/migrate v3.0.2-0.20170605201158-c1134be427e4+incompatible h1:acTvTtO6+8kylX0IshwhiGSLsJuWeWYYolLytDmD7pw= +github.com/mattes/migrate v3.0.2-0.20170605201158-c1134be427e4+incompatible/go.mod h1:LJcqgpj1jQoxv3m2VXd3drv0suK5CbN/RCX7MXwgnVI= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +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/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/handler/accounts.go b/handler/accounts.go new file mode 100644 index 0000000..6289892 --- /dev/null +++ b/handler/accounts.go @@ -0,0 +1,57 @@ +package handler + +import ( + "fmt" + "net/http" + + e "github.com/RealImage/QLedger/errors" + "github.com/RealImage/QLedger/models" + "github.com/RealImage/QLedger/utils" +) + +// AddAccount creates a new account with the input ID and data +func (s *Service) AddAccount(w http.ResponseWriter, r *http.Request) { + account := &models.Account{} + err := utils.UnmarshalToAccount(r, account) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + err = s.Ctrl.AddAccount(account) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + utils.WriteResponse(w, nil, http.StatusCreated) +} + +// GetAccounts returns the list of accounts that matches the search query +func (s *Service) GetAccounts(w http.ResponseWriter, r *http.Request) { + query, err := utils.ParseString(w, r) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + result, err := s.Ctrl.GetAccounts(query) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + utils.WriteResponse(w, result, http.StatusOK) +} + +// UpdateAccount updates data of an account with the input ID +func (s *Service) UpdateAccount(w http.ResponseWriter, r *http.Request) { + var account models.Account + err := utils.UnmarshalToAccount(r, &account) + if err != nil { + utils.WriteErrorStatus(w, fmt.Errorf("%w Error loading payload: %v", e.ErrBadRequest, err)) + return + } + err = s.Ctrl.UpdateAccount(&account) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + utils.WriteResponse(w, nil, http.StatusOK) +} diff --git a/controllers/accounts_search_test.go b/handler/accounts_search_test.go similarity index 85% rename from controllers/accounts_search_test.go rename to handler/accounts_search_test.go index be74b96..7d22980 100644 --- a/controllers/accounts_search_test.go +++ b/handler/accounts_search_test.go @@ -1,4 +1,4 @@ -package controllers +package handler import ( "bytes" @@ -10,8 +10,7 @@ import ( "os" "testing" - ledgerContext "github.com/RealImage/QLedger/context" - "github.com/RealImage/QLedger/middlewares" + "github.com/RealImage/QLedger/controller" "github.com/RealImage/QLedger/models" _ "github.com/lib/pq" @@ -25,7 +24,7 @@ var ( type AccountsSearchSuite struct { suite.Suite - context *ledgerContext.AppContext + handler Service } func (as *AccountsSearchSuite) SetupTest() { @@ -37,7 +36,14 @@ func (as *AccountsSearchSuite) SetupTest() { log.Panic("Unable to connect to Database:", err) } log.Println("Successfully established connection to database.") - as.context = &ledgerContext.AppContext{DB: db} + searchEngine, appErr := models.NewSearchEngine(db, models.SearchNamespaceAccounts) + if appErr != nil { + t.Fatal(appErr) + } + accountsDB := models.NewAccountDB(db) + transactionsDB := models.NewTransactionDB(db) + ctrl := controller.NewController(searchEngine, &accountsDB, &transactionsDB) + as.handler = Service{Ctrl: ctrl} // Create test accounts accDB := models.NewAccountDB(db) @@ -87,13 +93,12 @@ func (as *AccountsSearchSuite) TestAccountsSearch() { } } }` - handler := middlewares.ContextMiddleware(GetAccounts, as.context) req, err := http.NewRequest("GET", AccountSearchAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) + as.handler.GetAccounts(rr, req) assert.Equal(t, http.StatusOK, rr.Code, "Invalid response code") var accounts []models.AccountResult diff --git a/controllers/monitors.go b/handler/monitors.go similarity index 53% rename from controllers/monitors.go rename to handler/monitors.go index 139869e..9222876 100644 --- a/controllers/monitors.go +++ b/handler/monitors.go @@ -1,14 +1,18 @@ -package controllers +package handler import ( "net/http" + + "github.com/RealImage/QLedger/utils" ) // Ping responds 200 OK when the server is up and healthy func Ping(w http.ResponseWriter, r *http.Request) { // TODO: Should DB connection check be made while ping ? - response := `{"ping": "pong"}` - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(response)) - return + pong := struct { + Ping string `json:"ping"` + }{ + Ping: "pong", + } + utils.WriteResponse(w, &pong, http.StatusOK) } diff --git a/handler/server.go b/handler/server.go new file mode 100644 index 0000000..73f4458 --- /dev/null +++ b/handler/server.go @@ -0,0 +1,47 @@ +package handler + +import ( + "net/http" + + ctrl "github.com/RealImage/QLedger/controller" + "github.com/RealImage/QLedger/middlewares" + "github.com/julienschmidt/httprouter" +) + +type Service struct { + Ctrl *ctrl.Controller +} + +func AccountRouter(hostPrefix string, router *httprouter.Router, ctrl *ctrl.Controller) { + service := Service{Ctrl: ctrl} + router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/accounts", + middlewares.TokenAuthMiddleware(service.AddAccount)) + router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/transactions", + middlewares.TokenAuthMiddleware(service.MakeTransaction)) + // Read or search accounts and transactions + router.HandlerFunc(http.MethodGet, hostPrefix+"/v1/accounts", + middlewares.TokenAuthMiddleware(service.GetAccounts)) + router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/accounts/_search", + middlewares.TokenAuthMiddleware(service.GetAccounts)) +} + +func TransactionRouter(hostPrefix string, router *httprouter.Router, ctrl *ctrl.Controller) { + service := Service{Ctrl: ctrl} + router.HandlerFunc(http.MethodGet, hostPrefix+"/v1/transactions", + middlewares.TokenAuthMiddleware(service.GetTransactions)) + router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/transactions/_search", + middlewares.TokenAuthMiddleware(service.GetTransactions)) + // Update data of accounts and transactions + router.HandlerFunc(http.MethodPut, hostPrefix+"/v1/accounts", + middlewares.TokenAuthMiddleware(service.UpdateAccount)) + router.HandlerFunc(http.MethodPut, hostPrefix+"/v1/transactions", + middlewares.TokenAuthMiddleware(service.UpdateTransaction)) +} + +func NewRouter(hostPrefix string, accCtrl *ctrl.Controller, trCtrl *ctrl.Controller) *httprouter.Router { + router := httprouter.New() + router.HandlerFunc(http.MethodGet, hostPrefix+"/ping", Ping) + AccountRouter(hostPrefix, router, accCtrl) + TransactionRouter(hostPrefix, router, trCtrl) + return router +} diff --git a/handler/transactions.go b/handler/transactions.go new file mode 100644 index 0000000..0cc8000 --- /dev/null +++ b/handler/transactions.go @@ -0,0 +1,59 @@ +package handler + +import ( + "net/http" + + "github.com/RealImage/QLedger/models" + "github.com/RealImage/QLedger/utils" +) + +// MakeTransaction creates a new transaction from the request data +func (s *Service) MakeTransaction(w http.ResponseWriter, r *http.Request) { + var transaction models.Transaction + err := utils.UnmarshalToTransaction(r, &transaction) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + exists, err := s.Ctrl.MakeTransaction(&transaction) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + if !exists { + utils.WriteResponse(w, nil, http.StatusCreated) + return + } + utils.WriteResponse(w, nil, http.StatusAccepted) +} + +// GetTransactions returns the list of transactions that matches the search query +func (s *Service) GetTransactions(w http.ResponseWriter, r *http.Request) { + query, err := utils.ParseString(w, r) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + results, err := s.Ctrl.GetTransactions(query) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + utils.WriteResponse(w, results, http.StatusOK) +} + +// UpdateTransaction updates the data of a transaction with the input ID +func (s *Service) UpdateTransaction(w http.ResponseWriter, r *http.Request) { + var transaction models.Transaction + err := utils.UnmarshalToTransaction(r, &transaction) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + err = s.Ctrl.UpdateTransaction(&transaction) + if err != nil { + utils.WriteErrorStatus(w, err) + return + } + utils.WriteResponse(w, nil, http.StatusOK) +} diff --git a/controllers/transactions_search_test.go b/handler/transactions_search_test.go similarity index 88% rename from controllers/transactions_search_test.go rename to handler/transactions_search_test.go index b42200d..c6244c7 100644 --- a/controllers/transactions_search_test.go +++ b/handler/transactions_search_test.go @@ -1,4 +1,4 @@ -package controllers +package handler import ( "bytes" @@ -10,8 +10,7 @@ import ( "os" "testing" - ledgerContext "github.com/RealImage/QLedger/context" - "github.com/RealImage/QLedger/middlewares" + "github.com/RealImage/QLedger/controller" "github.com/RealImage/QLedger/models" _ "github.com/lib/pq" @@ -25,7 +24,7 @@ var ( type TransactionSearchSuite struct { suite.Suite - context *ledgerContext.AppContext + handler Service } func (as *TransactionSearchSuite) SetupTest() { @@ -37,8 +36,14 @@ func (as *TransactionSearchSuite) SetupTest() { log.Panic("Unable to connect to Database:", err) } log.Println("Successfully established connection to database.") - as.context = &ledgerContext.AppContext{DB: db} - + searchEngine, appErr := models.NewSearchEngine(db, models.SearchNamespaceTransactions) + if appErr != nil { + t.Fatal(appErr) + } + accountsDB := models.NewAccountDB(db) + transactionsDB := models.NewTransactionDB(db) + ctrl := controller.NewController(searchEngine, &accountsDB, &transactionsDB) + as.handler = Service{Ctrl: ctrl} // Create test transactions txnDB := models.NewTransactionDB(db) txn1 := &models.Transaction{ @@ -129,13 +134,13 @@ func (as *TransactionSearchSuite) TestTransactionsSearch() { } } }` - handler := middlewares.ContextMiddleware(GetTransactions, as.context) + req, err := http.NewRequest("GET", TransactionsSearchAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) + as.handler.GetTransactions(rr, req) assert.Equal(t, http.StatusOK, rr.Code, "Invalid response code") var transactions []models.TransactionResult diff --git a/controllers/transactions_test.go b/handler/transactions_test.go similarity index 80% rename from controllers/transactions_test.go rename to handler/transactions_test.go index 8181f57..07ba374 100644 --- a/controllers/transactions_test.go +++ b/handler/transactions_test.go @@ -1,4 +1,4 @@ -package controllers +package handler import ( "bytes" @@ -9,8 +9,8 @@ import ( "os" "testing" - ledgerContext "github.com/RealImage/QLedger/context" - "github.com/RealImage/QLedger/middlewares" + "github.com/RealImage/QLedger/controller" + "github.com/RealImage/QLedger/models" _ "github.com/lib/pq" "github.com/stretchr/testify/assert" @@ -23,7 +23,8 @@ var ( type TransactionsSuite struct { suite.Suite - context *ledgerContext.AppContext + handler Service + db *sql.DB } func (ts *TransactionsSuite) SetupSuite() { @@ -35,7 +36,15 @@ func (ts *TransactionsSuite) SetupSuite() { log.Panic("Unable to connect to Database:", err) } log.Println("Successfully established connection to database.") - ts.context = &ledgerContext.AppContext{DB: db} + ts.db = db + searchEngine, appErr := models.NewSearchEngine(db, models.SearchNamespaceTransactions) + if appErr != nil { + log.Panic(appErr) + } + accountsDB := models.NewAccountDB(db) + transactionsDB := models.NewTransactionDB(db) + ctrl := controller.NewController(searchEngine, &accountsDB, &transactionsDB) + ts.handler = Service{Ctrl: ctrl} } func (ts *TransactionsSuite) TestValidAndRepeatedTransaction() { @@ -59,13 +68,12 @@ func (ts *TransactionsSuite) TestValidAndRepeatedTransaction() { "tag_two": "val2" } }` - handler := middlewares.ContextMiddleware(MakeTransaction, ts.context) req, err := http.NewRequest("POST", TransactionsAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } rr1 := httptest.NewRecorder() - handler.ServeHTTP(rr1, req) + ts.handler.MakeTransaction(rr1, req) assert.Equal(t, http.StatusCreated, rr1.Code, "Invalid response code") // Duplicate transaction @@ -74,7 +82,7 @@ func (ts *TransactionsSuite) TestValidAndRepeatedTransaction() { t.Fatal(err) } rr2 := httptest.NewRecorder() - handler.ServeHTTP(rr2, req) + ts.handler.MakeTransaction(rr2, req) assert.Equal(t, http.StatusAccepted, rr2.Code, "Invalid response code") // Conflict transaction @@ -96,7 +104,7 @@ func (ts *TransactionsSuite) TestValidAndRepeatedTransaction() { t.Fatal(err) } rr3 := httptest.NewRecorder() - handler.ServeHTTP(rr3, req) + ts.handler.MakeTransaction(rr3, req) assert.Equal(t, http.StatusConflict, rr3.Code, "Invalid response code") } @@ -108,12 +116,11 @@ func (ts *TransactionsSuite) TestNoOpTransaction() { "lines": [] }` - handler := middlewares.ContextMiddleware(MakeTransaction, ts.context) req, err := http.NewRequest("POST", TransactionsAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } - handler.ServeHTTP(rr, req) + ts.handler.MakeTransaction(rr, req) assert.Equal(t, http.StatusCreated, rr.Code, "Invalid response code") } @@ -135,12 +142,11 @@ func (ts *TransactionsSuite) TestInvalidTransaction() { ] }` - handler := middlewares.ContextMiddleware(MakeTransaction, ts.context) req, err := http.NewRequest("POST", TransactionsAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } - handler.ServeHTTP(rr, req) + ts.handler.MakeTransaction(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code, "Invalid response code") } @@ -152,12 +158,11 @@ func (ts *TransactionsSuite) TestBadTransaction() { INVALID PAYLOAD }` - handler := middlewares.ContextMiddleware(MakeTransaction, ts.context) req, err := http.NewRequest("POST", TransactionsAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } - handler.ServeHTTP(rr, req) + ts.handler.MakeTransaction(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code, "Invalid response code") } @@ -181,14 +186,20 @@ func (ts *TransactionsSuite) TestFailTransaction() { // database is not available db, _ := sql.Open("postgres", "") - invalidContext := &ledgerContext.AppContext{DB: db} + searchEngine, appErr := models.NewSearchEngine(db, models.SearchNamespaceTransactions) + if appErr != nil { + log.Panic(appErr) + } + accountsDB := models.NewAccountDB(db) + transactionsDB := models.NewTransactionDB(db) + ctrl := controller.NewController(searchEngine, &accountsDB, &transactionsDB) + h := Service{Ctrl: ctrl} - handler := middlewares.ContextMiddleware(MakeTransaction, invalidContext) req, err := http.NewRequest("POST", TransactionsAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } - handler.ServeHTTP(rr, req) + h.MakeTransaction(rr, req) assert.Equal(t, http.StatusInternalServerError, rr.Code, "Invalid response code") } @@ -214,13 +225,12 @@ func (ts *TransactionsSuite) TestCreateTransactionWithBoundaryValues() { "tag_two": "val2" } }` - handler := middlewares.ContextMiddleware(MakeTransaction, ts.context) req, err := http.NewRequest("POST", TransactionsAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } rr1 := httptest.NewRecorder() - handler.ServeHTTP(rr1, req) + ts.handler.MakeTransaction(rr1, req) assert.Equal(t, http.StatusCreated, rr1.Code, "Invalid response code") // Out-of-boundary value transaction @@ -241,13 +251,12 @@ func (ts *TransactionsSuite) TestCreateTransactionWithBoundaryValues() { "tag_two": "val2" } }` - handler = middlewares.ContextMiddleware(MakeTransaction, ts.context) req, err = http.NewRequest("POST", TransactionsAPI, bytes.NewBufferString(payload)) if err != nil { t.Fatal(err) } rr1 = httptest.NewRecorder() - handler.ServeHTTP(rr1, req) + ts.handler.MakeTransaction(rr1, req) assert.Equal(t, http.StatusBadRequest, rr1.Code, "Invalid response code") } @@ -255,15 +264,15 @@ func (ts *TransactionsSuite) TearDownSuite() { log.Println("Cleaning up the test database") t := ts.T() - _, err := ts.context.DB.Exec(`DELETE FROM lines`) + _, err := ts.db.Exec(`DELETE FROM lines`) if err != nil { t.Fatal("Error deleting lines:", err) } - _, err = ts.context.DB.Exec(`DELETE FROM transactions`) + _, err = ts.db.Exec(`DELETE FROM transactions`) if err != nil { t.Fatal("Error deleting transactions:", err) } - _, err = ts.context.DB.Exec(`DELETE FROM accounts`) + _, err = ts.db.Exec(`DELETE FROM accounts`) if err != nil { t.Fatal("Error deleting accounts:", err) } diff --git a/main.go b/main.go index 9a63cbd..d4e4d6c 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,9 @@ import ( "net/http" "os" - ledgerContext "github.com/RealImage/QLedger/context" - "github.com/RealImage/QLedger/controllers" - "github.com/RealImage/QLedger/middlewares" - "github.com/julienschmidt/httprouter" + "github.com/RealImage/QLedger/controller" + "github.com/RealImage/QLedger/handler" + "github.com/RealImage/QLedger/models" "github.com/mattes/migrate" "github.com/mattes/migrate/database" "github.com/mattes/migrate/database/postgres" @@ -32,42 +31,22 @@ func main() { // Migrate DB changes migrateDB(db) - appContext := &ledgerContext.AppContext{DB: db} - router := httprouter.New() - - hostPrefix := os.Getenv("HOST_PREFIX") - // Monitors - router.HandlerFunc(http.MethodGet, hostPrefix+"/ping", controllers.Ping) - - // Create accounts and transactions - router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/accounts", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.AddAccount, appContext))) - router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/transactions", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.MakeTransaction, appContext))) + accSearchEngine, appErr := models.NewSearchEngine(db, models.SearchNamespaceAccounts) + if appErr != nil { + log.Fatal(appErr) + } - // Read or search accounts and transactions - router.HandlerFunc(http.MethodGet, hostPrefix+"/v1/accounts", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.GetAccounts, appContext))) - router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/accounts/_search", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.GetAccounts, appContext))) - router.HandlerFunc(http.MethodGet, hostPrefix+"/v1/transactions", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.GetTransactions, appContext))) - router.HandlerFunc(http.MethodPost, hostPrefix+"/v1/transactions/_search", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.GetTransactions, appContext))) + trSearchEngine, appErr := models.NewSearchEngine(db, models.SearchNamespaceTransactions) + if appErr != nil { + log.Fatal(appErr) + } - // Update data of accounts and transactions - router.HandlerFunc(http.MethodPut, hostPrefix+"/v1/accounts", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.UpdateAccount, appContext))) - router.HandlerFunc(http.MethodPut, hostPrefix+"/v1/transactions", - middlewares.TokenAuthMiddleware( - middlewares.ContextMiddleware(controllers.UpdateTransaction, appContext))) + accountDB := models.NewAccountDB(db) + transactionDB := models.NewTransactionDB(db) + accCtrl := controller.NewController(accSearchEngine, &accountDB, &transactionDB) + trCtrl := controller.NewController(trSearchEngine, &accountDB, &transactionDB) + hostPrefix := os.Getenv("HOST_PREFIX") + router := handler.NewRouter(hostPrefix, accCtrl, trCtrl) port := os.Getenv("PORT") if port == "" { @@ -75,12 +54,6 @@ func main() { } log.Println("Running server on port:", port) log.Fatal(http.ListenAndServe(":"+port, router)) - - defer func() { - if r := recover(); r != nil { - log.Println("Server exited!!!", r) - } - }() } func migrateDB(db *sql.DB) { diff --git a/middlewares/context.go b/middlewares/context.go deleted file mode 100644 index 5fca3ee..0000000 --- a/middlewares/context.go +++ /dev/null @@ -1,17 +0,0 @@ -package middlewares - -import ( - "net/http" - - ledgerContext "github.com/RealImage/QLedger/context" -) - -// Handler is a custom HTTP handler that has an additional application context -type Handler func(http.ResponseWriter, *http.Request, *ledgerContext.AppContext) - -// ContextMiddleware is a middleware that provides application context to the `Handler` -func ContextMiddleware(handler Handler, context *ledgerContext.AppContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - handler(w, r, context) - } -} diff --git a/models/search.go b/models/search.go index 5e94caa..31ce409 100644 --- a/models/search.go +++ b/models/search.go @@ -58,7 +58,7 @@ func NewSearchEngine(db *sql.DB, namespace string) (*SearchEngine, ledgerError.A return &SearchEngine{db: db, namespace: namespace}, nil } -// Query returns the results of a searc query +// Query returns the results of a search query func (engine *SearchEngine) Query(q string) (interface{}, ledgerError.ApplicationError) { rawQuery, aerr := NewSearchRawQuery(q) if aerr != nil { @@ -196,7 +196,7 @@ func (rawQuery *SearchRawQuery) ToSQLQuery(namespace string) *SearchSQLQuery { case SearchNamespaceAccounts: q = "SELECT id, balance, data FROM current_balances" case SearchNamespaceTransactions: - q = `SELECT id, timestamp, data, + q = `SELECT transactions.id, timestamp, data, array_to_json(ARRAY( SELECT lines.account_id FROM lines WHERE transaction_id=transactions.id @@ -207,7 +207,8 @@ func (rawQuery *SearchRawQuery) ToSQLQuery(namespace string) *SearchSQLQuery { WHERE transaction_id=transactions.id ORDER BY lines.account_id )) AS delta_array - FROM transactions` + FROM transactions + LEFT JOIN lines ON lines.transaction_id = transactions.id` default: return nil } @@ -262,6 +263,7 @@ func (rawQuery *SearchRawQuery) ToSQLQuery(namespace string) *SearchSQLQuery { } if namespace == SearchNamespaceTransactions { + q += " GROUP BY transactions.id" if rawQuery.SortTime == SortDescByTime { q += " ORDER BY timestamp DESC" } else { diff --git a/models/searchutils.go b/models/searchutils.go index 3a3cb3c..66141a2 100644 --- a/models/searchutils.go +++ b/models/searchutils.go @@ -176,6 +176,9 @@ func convertFieldsToSQL(fields []map[string]map[string]interface{}) (where []str for _, field := range fields { var conditions []string for key, comparison := range field { + if key == "id" { + key = "transaction.id" + } for op, value := range comparison { condn := fmt.Sprintf("%s %s ?", key, sqlComparisonOp(op)) conditions = append(conditions, condn) diff --git a/tests/transactions_test.go b/tests/transactions_test.go index 0c6e481..d45bf58 100644 --- a/tests/transactions_test.go +++ b/tests/transactions_test.go @@ -17,9 +17,8 @@ import ( "testing" "time" - ledgerContext "github.com/RealImage/QLedger/context" - "github.com/RealImage/QLedger/controllers" - "github.com/RealImage/QLedger/middlewares" + "github.com/RealImage/QLedger/controller" + "github.com/RealImage/QLedger/handler" "github.com/RealImage/QLedger/models" _ "github.com/lib/pq" "github.com/stretchr/testify/suite" @@ -244,9 +243,17 @@ func VerifyExpectedBalance(endpoint string, accounts []map[string]interface{}) { type CSVSuite struct { suite.Suite - context *ledgerContext.AppContext accountServer *httptest.Server transactionsServer *httptest.Server + db *sql.DB +} + +type testHandler struct { + handler func(w http.ResponseWriter, r *http.Request) +} + +func (t *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + t.handler(w, r) } func (cs *CSVSuite) SetupTest() { @@ -257,9 +264,17 @@ func (cs *CSVSuite) SetupTest() { } log.Println("Successfully established connection to database.") log.Println("Starting test endpoints...") - cs.context = &ledgerContext.AppContext{DB: db} - cs.accountServer = httptest.NewServer(middlewares.ContextMiddleware(controllers.GetAccounts, cs.context)) - cs.transactionsServer = httptest.NewServer(middlewares.ContextMiddleware(controllers.MakeTransaction, cs.context)) + cs.db = db + searchEngine, appErr := models.NewSearchEngine(db, models.SearchNamespaceTransactions) + if appErr != nil { + log.Panic(appErr) + } + accountsDB := models.NewAccountDB(db) + transactionsDB := models.NewTransactionDB(db) + ctrl := controller.NewController(searchEngine, &accountsDB, &transactionsDB) + h := handler.Service{Ctrl: ctrl} + cs.accountServer = httptest.NewServer(&testHandler{handler: h.GetAccounts}) + cs.transactionsServer = httptest.NewServer(&testHandler{handler: h.MakeTransaction}) } func (cs *CSVSuite) TestTransactionsLoad() { @@ -274,15 +289,15 @@ func (cs *CSVSuite) TearDownTest() { log.Println("Cleaning up the test database") t := cs.T() - _, err := cs.context.DB.Exec(`DELETE FROM lines`) + _, err := cs.db.Exec(`DELETE FROM lines`) if err != nil { t.Fatal("Error deleting lines:", err) } - _, err = cs.context.DB.Exec(`DELETE FROM transactions`) + _, err = cs.db.Exec(`DELETE FROM transactions`) if err != nil { t.Fatal("Error deleting transactions:", err) } - _, err = cs.context.DB.Exec(`DELETE FROM accounts`) + _, err = cs.db.Exec(`DELETE FROM accounts`) if err != nil { t.Fatal("Error deleting accounts:", err) } diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..2644015 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,91 @@ +package utils + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "regexp" + "time" + + e "github.com/RealImage/QLedger/errors" + "github.com/RealImage/QLedger/models" +) + +func WriteErrorStatus(w http.ResponseWriter, err error) { + var status int + switch { + case errors.Is(err, e.ErrBadRequest): + status = http.StatusBadRequest + case errors.Is(err, e.ErrConflict): + status = http.StatusConflict + case errors.Is(err, e.ErrNotFound): + status = http.StatusNotFound + default: + status = http.StatusInternalServerError + } + log.Println(err) + w.WriteHeader(status) +} + +func UnmarshalToAccount(r *http.Request, account *models.Account) error { + err := json.NewDecoder(r.Body).Decode(account) + if err != nil { + return fmt.Errorf("%w %v", e.ErrBadRequest, err) + } + var validKey = regexp.MustCompile(`^[a-z_A-Z]+$`) + for key := range account.Data { + if !validKey.MatchString(key) { + return fmt.Errorf("%w: Invalid key in data json: %v", e.ErrBadRequest, key) + } + } + return nil +} + +func UnmarshalToTransaction(r *http.Request, txn *models.Transaction) error { + err := json.NewDecoder(r.Body).Decode(txn) + if err != nil { + return fmt.Errorf("%w %v", e.ErrBadRequest, err) + } + var validKey = regexp.MustCompile(`^[a-z_A-Z]+$`) + for key := range txn.Data { + if !validKey.MatchString(key) { + return fmt.Errorf("%w: Invalid key in data json: %v", e.ErrBadRequest, key) + } + } + // Validate timestamp format if present + if txn.Timestamp != "" { + _, err := time.Parse(models.LedgerTimestampLayout, txn.Timestamp) + if err != nil { + return fmt.Errorf("%w: %v", e.ErrBadRequest, err) + } + } + + return nil +} + +func WriteResponse(w http.ResponseWriter, data interface{}, status int) { + if data != nil { + data, err := json.Marshal(data) + if err != nil { + WriteErrorStatus(w, fmt.Errorf("%w: %v", e.ErrInternal, err)) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _, err = w.Write(data) + if err != nil { + log.Println(err) + } + } + w.WriteHeader(status) +} + +func ParseString(w http.ResponseWriter, r *http.Request) (string, error) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", fmt.Errorf("%w: error reading payload: %v", e.ErrBadRequest, err) + } + return string(body), nil +}