diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index db210cf..7f4624d 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -2,14 +2,17 @@ package main import ( "fmt" + "log" "net/http" + "os" "strings" "time" - "git.rosemyrtle.work/personal-finance/server/internal/db" - "git.rosemyrtle.work/personal-finance/server/internal/model" + "git.rosemyrtle.work/personal-finance/server/internal/dal" + "git.rosemyrtle.work/personal-finance/server/internal/entities" "github.com/gin-gonic/gin" + "github.com/joho/godotenv" "github.com/shopspring/decimal" ) @@ -29,22 +32,18 @@ type Bank struct { NordigenID string `json:"nordigenId,omitempty"` } -// ResponseError struct represents an error response -type ResponseError struct { - Message string `json:"message"` +type Env struct { + db *dal.Connection } -// Handlers - -func retrieveTransactions(c *gin.Context) { +func (e *Env) retrieveTransactions(c *gin.Context) { // Handle the logic for retrieving transactions here // limit := c.Query("limit") // offset := c.Query("offset") // bank := c.Query("bank") // sort := c.Query("sort") - db := db.Handle{} - transactions := db.Transactions() + transactions := e.db.Transactions() var ret []Transaction for _, t := range transactions { @@ -57,12 +56,12 @@ func retrieveTransactions(c *gin.Context) { c.JSON(http.StatusOK, ret) } -func retrieveTransactionByID(c *gin.Context) { +func (e *Env) retrieveTransactionByID(c *gin.Context) { // Handle the logic for retrieving a transaction by ID here // transactionID := c.Param("transactionId") // Placeholder response - transaction := model.Transaction{ + transaction := entities.Transaction{ Id: 1, Date: time.Date(2024, 01, 24, 0, 0, 0, 0, time.UTC), Description: "Groceries", @@ -73,7 +72,7 @@ func retrieveTransactionByID(c *gin.Context) { c.JSON(http.StatusOK, transaction) } -func retrieveBanks(c *gin.Context) { +func (e *Env) retrieveBanks(c *gin.Context) { // Handle the logic for retrieving banks here // Placeholder response @@ -85,7 +84,7 @@ func retrieveBanks(c *gin.Context) { c.JSON(http.StatusOK, banks) } -func retrieveBankByID(c *gin.Context) { +func (e *Env) retrieveBankByID(c *gin.Context) { // Handle the logic for retrieving a bank by ID here bankID := c.Param("bankId") @@ -96,13 +95,33 @@ func retrieveBankByID(c *gin.Context) { } func main() { + if err := godotenv.Load(); err != nil { + log.Fatal("Error loading .env file") + return + } + + user := os.Getenv("PG_USER") + pass := os.Getenv("PG_PASSWORD") + location := os.Getenv("PG_URL") + database := os.Getenv("PG_DB") + + credentials := entities.Credentials{Database: "postgres://" + user + ":" + pass + "@" + location + "/" + database} + db, err := dal.Open(&credentials) + if err != nil { + log.Fatal(err) + return + } + defer db.Close() + router := gin.Default() + env := Env{db: db} + // Routes - router.GET("/transactions", retrieveTransactions) - router.GET("/transactions/:transactionId", retrieveTransactionByID) - router.GET("/banks", retrieveBanks) - router.GET("/banks/:bankId", retrieveBankByID) + router.GET("/transactions", env.retrieveTransactions) + router.GET("/transactions/:transactionId", env.retrieveTransactionByID) + router.GET("/banks", env.retrieveBanks) + router.GET("/banks/:bankId", env.retrieveBankByID) // Start server port := 8080 diff --git a/go.mod b/go.mod index 330aeb1..8adc96e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.1 require github.com/gin-gonic/gin v1.9.1 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect @@ -13,6 +14,11 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.3 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect @@ -24,10 +30,11 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8d35c7c..73e3284 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -23,8 +25,19 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= +github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= @@ -61,14 +74,22 @@ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= diff --git a/internal/dal/dal.go b/internal/dal/dal.go new file mode 100644 index 0000000..6a31ae9 --- /dev/null +++ b/internal/dal/dal.go @@ -0,0 +1,60 @@ +package dal + +import ( + "database/sql" + "errors" + "fmt" + "log" + "os" + + "git.rosemyrtle.work/personal-finance/server/internal/entities" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +type Connection struct { + db *sql.DB +} + +func Open(credentials *entities.Credentials) (*Connection, error) { + if credentials == nil { + return nil, errors.New("missing credentials") + } + + db, err := sql.Open("pgx", credentials.Database) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) + return nil, err + } + + if err := db.Ping(); err != nil { + log.Fatal(err) + } + + return &Connection{db}, nil +} + +func (c *Connection) Close() { + c.db.Close() +} + +func (conn *Connection) Transactions() []entities.Transaction { + q := "SELECT t.date, t.description, t.amount FROM transactions t;" + + rows, err := conn.db.Query(q) + if err != nil { + log.Fatal(err) + } + + var result []entities.Transaction + + for rows.Next() { + var t entities.Transaction + if err := rows.Scan(&t.Date, &t.Description, &t.Value); err != nil { + log.Fatal(err) + } + result = append(result, t) + } + + return result +} diff --git a/internal/dal/dal_test.go b/internal/dal/dal_test.go new file mode 100644 index 0000000..2684e10 --- /dev/null +++ b/internal/dal/dal_test.go @@ -0,0 +1,43 @@ +package dal + +import ( + "database/sql/driver" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" +) + +type AnyTime struct{} + +func (t AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} + +// type AnyDecimal struct{} + +func TestTransactions(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to create a mock DB: %s", err) + } + defer db.Close() + + conn := Connection{db} + + rows := sqlmock.NewRows([]string{"date", "description", "amount"}). + AddRow(time.Now(), "income", 1000). + AddRow(time.Now(), "expense", -500) + + mock.ExpectQuery("SELECT t.date, t.description, t.amount").WithoutArgs().WillReturnRows(rows) + + transactions := conn.Transactions() + if len(transactions) <= 0 { + t.Error("No transactions returned") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %s", err) + } +} diff --git a/internal/db/db.go b/internal/db/db.go deleted file mode 100644 index 64da321..0000000 --- a/internal/db/db.go +++ /dev/null @@ -1,17 +0,0 @@ -package db - -import ( - "time" - - "git.rosemyrtle.work/personal-finance/server/internal/model" - "github.com/shopspring/decimal" -) - -type Handle struct{} - -func (*Handle) Transactions() []model.Transaction { - return []model.Transaction{ - {Id: 1, Date: time.Date(2024, 01, 24, 0, 0, 0, 0, time.UTC), Description: "Groceries", Value: decimal.NewFromFloat(-50.0), Category: "Food"}, - {Id: 2, Date: time.Date(2024, 01, 23, 0, 0, 0, 0, time.UTC), Description: "Salary", Value: decimal.NewFromFloat(3000.0), Category: "Income"}, - } -} diff --git a/internal/entities/credentials.go b/internal/entities/credentials.go new file mode 100644 index 0000000..7f51bba --- /dev/null +++ b/internal/entities/credentials.go @@ -0,0 +1,5 @@ +package entities + +type Credentials struct { + Database string +} diff --git a/internal/model/entities.go b/internal/entities/entities.go similarity index 91% rename from internal/model/entities.go rename to internal/entities/entities.go index 5f7ad13..fd4a200 100644 --- a/internal/model/entities.go +++ b/internal/entities/entities.go @@ -1,4 +1,4 @@ -package model +package entities import ( "time"