Working /transactions endpoint with DB connection
Adds the database abstraction layer (DAL) with a Transactions method and a Connection object to encapsulate the DB open connection. Includes a DAL unit test using the go-sqlmock library. Connection to a PostgreSQL database using environment variables to store the secrets. Overall, with the /transactions endpoint requesting directly to the DB, this patch finish the tracer bullet project. Issues #1, #5, #6 and #7
This commit is contained in:
parent
a7359aacea
commit
19974a5f44
@ -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
|
||||
|
||||
13
go.mod
13
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
|
||||
)
|
||||
|
||||
21
go.sum
21
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=
|
||||
|
||||
60
internal/dal/dal.go
Normal file
60
internal/dal/dal.go
Normal file
@ -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
|
||||
}
|
||||
43
internal/dal/dal_test.go
Normal file
43
internal/dal/dal_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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"},
|
||||
}
|
||||
}
|
||||
5
internal/entities/credentials.go
Normal file
5
internal/entities/credentials.go
Normal file
@ -0,0 +1,5 @@
|
||||
package entities
|
||||
|
||||
type Credentials struct {
|
||||
Database string
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
Loading…
x
Reference in New Issue
Block a user