Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

11 changed files with 59 additions and 116 deletions

View File

@ -2,7 +2,7 @@
Simple api used for tracking holidays Simple api used for tracking holidays
To check out application, open [https://holiday.bbr-dev.info](https://holiday.bbr-dev.info) To checkout application open [https://holiday.bbr-dev.info](https://holiday.bbr-dev.info)
## Endpoints ## Endpoints
@ -28,25 +28,25 @@ That endpoint accepts a list of required and optional parameters
- if defined year and rangeStart|rangeEnd parameters are ignored - if defined year and rangeStart|rangeEnd parameters are ignored
`range_start|range_end` `rangeStart|rangeEnd`
- returns holidays in given range with both ends being inclusive - returns holidays in given range with both ends being inclusive
- if either limit isn't defined it is assumed to be up to or all from given limit (if rangeStart isn't defined all holidays before rangeEnd are returned and vice-verse) - if either limit isn't defined it is assumed to be up to or all from given limit (if rangeStart isn't defined all holidays before rangeEnd are returned and vice-verse)
- dates must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html) - dates must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html)
- eg. `range_start=2021-12-25&range_end=2023-01-23`, `range_start=2023-01-20` - eg. `rangeStart=2021-12-25&rangeEnd=2023-01-23`, `rangeStart=2023-01-20`
- if defined year parameter is ignored - if defined year parameter is ignored
`state_holiday` `stateHoliday`
- if set true only holidays that are tagged as state holidays are returned, similar for if set false, if not set all holidays are returned - if set true only holidays that are tagged as state holidays are returned, similar for if set false, if not set all holidays are returned
- eg. `state_holiday=true`, `state_holiday=false` - eg. `stateHoliday=true`, `stateHoliday=false`
`religious_holiday` `religiousHoliday`
- if set true only holidays that are tagged as religious holidays are returned, similar for if set false, if not set all holidays are returned - if set true only holidays that are tagged as religious holidays are returned, similar for if set false, if not set all holidays are returned
- eg. `religious_holiday=true`, `religious_holiday=false` - eg. `religiousHoliday=true`, `religiousHoliday=false`
#### Paging #### Paging
`page_size` `pageSize`
- returns at most pageSize number of holidays - returns at most pageSize number of holidays
- eg. `page_size=20` - eg. `pageSize=20`
- only applied if page is defined as well, by default set to 20 - only applied if page is defined as well, by default set to 20
`page` `page`
@ -68,7 +68,7 @@ By default, responses are returned as a json array
},...] },...]
} }
``` ```
e.g. eg.
``` ```
{ {
"holidays": [{ "holidays": [{
@ -99,19 +99,4 @@ CSV Response
``` ```
id,date,name,description,is_state_holiday,is_religious_holiday id,date,name,description,is_state_holiday,is_religious_holiday
74a2a769-abf2-45d4-bdc4-442bbcc89138,2023-12-25,Christmas,TBD,true,true 74a2a769-abf2-45d4-bdc4-442bbcc89138,2023-12-25,Christmas,TBD,true,true
```
### Development
To start server few environment variables need to be set up. This can be done by creating `.env` file with following content
```bash
PSQL_HOST=localhost
PSQL_PORT=5432
PSQL_USER=holiday
PSQL_PASSWORD=holidayPassword
PSQL_DB=holiday
PROFILE=dev,basic-auth
AUTH_KEY=holiday:holidayPassword
``` ```

View File

@ -1,10 +1,11 @@
package api package api
import ( import (
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"holiday-api/domain/holiday" "holiday-api/domain/holiday"
"log/slog" "log"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -338,7 +339,7 @@ func render(c *gin.Context, status int, response any, contentType *string) {
} }
func Abort(c *gin.Context, err error, statusCode int, message string) { func Abort(c *gin.Context, err error, statusCode int, message string) {
slog.Error(message, slog.String("err", errorMessage(err))) log.Output(1, fmt.Sprintf("error | %s | err: %v", message, errorMessage(err)))
c.AbortWithError(statusCode, err) c.AbortWithError(statusCode, err)
} }

View File

@ -44,10 +44,10 @@ window.addEventListener('load', () => {
query['country'] = country.value; query['country'] = country.value;
query['contentType'] = contentType.value; query['contentType'] = contentType.value;
if(stateHoliday.value === 'true' || stateHoliday.value === 'false') { if(stateHoliday.value === 'true' || stateHoliday.value === 'false') {
query['state_holiday'] = parseBoolean(stateHoliday.value); query['stateHoliday'] = parseBoolean(stateHoliday.value);
} }
if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') { if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') {
query['religious_holiday'] = parseBoolean(religiousHoliday.value); query['religiousHoliday'] = parseBoolean(religiousHoliday.value);
} }
console.log(dateSelector.value, dStartRange.value, dEndRange.value); console.log(dateSelector.value, dStartRange.value, dEndRange.value);
switch(dateSelector.value) { switch(dateSelector.value) {
@ -61,10 +61,10 @@ window.addEventListener('load', () => {
break; break;
case 'range': case 'range':
if(dStartRange.value) { if(dStartRange.value) {
query['range_start'] = dStartRange.value; query['startRange'] = dStartRange.value;
} }
if(dEndRange.value) { if(dEndRange.value) {
query['range_end'] = dEndRange.value; query['endRange'] = dEndRange.value;
} }
case 'all': case 'all':
default: default:
@ -78,10 +78,10 @@ window.addEventListener('load', () => {
query['country'] = country.value; query['country'] = country.value;
query['type'] = contentType.value; query['type'] = contentType.value;
if(stateHoliday.value === 'true' || stateHoliday.value === 'false') { if(stateHoliday.value === 'true' || stateHoliday.value === 'false') {
query['state_holiday'] = parseBoolean(stateHoliday.value); query['stateHoliday'] = parseBoolean(stateHoliday.value);
} }
if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') { if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') {
query['religious_holiday'] = parseBoolean(religiousHoliday.value); query['religiousHoliday'] = parseBoolean(religiousHoliday.value);
} }
console.log(dateSelector.value, dStartRange.value, dEndRange.value); console.log(dateSelector.value, dStartRange.value, dEndRange.value);
switch(dateSelector.value) { switch(dateSelector.value) {
@ -95,10 +95,10 @@ window.addEventListener('load', () => {
break; break;
case 'range': case 'range':
if(dStartRange.value) { if(dStartRange.value) {
query['range_start'] = dStartRange.value; query['startRange'] = dStartRange.value;
} }
if(dEndRange.value) { if(dEndRange.value) {
query['range_end'] = dEndRange.value; query['endRange'] = dEndRange.value;
} }
case 'all': case 'all':
default: default:

View File

@ -1,3 +0,0 @@
### v1.0.1
* Updated api documentation page
* Updated README.md

12
db.go
View File

@ -16,22 +16,12 @@ func envMustExist(env string) string {
} }
} }
func envOrDefault(env string, defaultValue string) string {
if value, exists := os.LookupEnv(env); exists {
return value
} else {
return defaultValue
}
}
func connectToDb() (*sqlx.DB, error) { func connectToDb() (*sqlx.DB, error) {
host := envMustExist("PSQL_HOST") host := envMustExist("PSQL_HOST")
port := envMustExist("PSQL_PORT") port := envMustExist("PSQL_PORT")
user := envMustExist("PSQL_USER") user := envMustExist("PSQL_USER")
password := envMustExist("PSQL_PASSWORD") password := envMustExist("PSQL_PASSWORD")
dbname := envMustExist("PSQL_DB") dbname := envMustExist("PSQL_DB")
sslMode := envOrDefault("PSQL_SSLMODE", "disable")
schema := envOrDefault("PSQL_SCHEMA", "public")
return db.ConnectToDbNamed(host, port, user, password, dbname, sslMode, schema) return db.ConnectToDbNamed(host, port, user, password, dbname)
} }

View File

@ -12,9 +12,9 @@ var DevMigrations embed.FS
//go:embed prod/*.sql //go:embed prod/*.sql
var ProdMigrations embed.FS var ProdMigrations embed.FS
func ConnectToDbNamed(host string, port string, user string, password string, dbname string, sslMode string, schema string) (*sqlx.DB, error) { func ConnectToDbNamed(host string, port string, user string, password string, dbname string) (*sqlx.DB, error) {
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s search_path=%s", psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname, sslMode, schema) host, port, user, password, dbname)
db, err := sqlx.Open("postgres", psqlInfo) db, err := sqlx.Open("postgres", psqlInfo)
if err != nil { if err != nil {

4
go.mod
View File

@ -1,10 +1,10 @@
module holiday-api module holiday-api
go 1.22 go 1.19
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.3.0
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9

2
go.sum
View File

@ -34,8 +34,6 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=

56
main.go
View File

@ -1,51 +1,36 @@
package main package main
import ( import (
"github.com/gin-gonic/gin"
"github.com/joho/godotenv" "github.com/joho/godotenv"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"holiday-api/api" "holiday-api/api"
"holiday-api/db" "holiday-api/db"
"holiday-api/migration" "holiday-api/migration"
"io/fs" "io/fs"
"log/slog" "log"
"net/http" "net/http"
"os" "os"
"runtime/debug"
"strings" "strings"
) )
func init() { func init() {
godotenv.Load() godotenv.Load()
if !hasProfile("dev") { log.SetPrefix("")
gin.SetMode(gin.ReleaseMode) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
}
if value := os.Getenv("LOG_FORMAT"); value == "json" {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})))
}
} }
func main() { func main() {
commit, time := buildInfo() db, err := connectToDb()
slog.Info("build info", slog.String("commit", commit), slog.String("time", time))
client, err := connectToDb()
if err != nil { if err != nil {
slog.Error("couldn't connect to client", slog.String("err", err.Error())) log.Fatalf("couldn't connect to db: %v", err)
os.Exit(1)
} }
if err := migration.InitializeMigrations(client, migrationFolder()); err != nil { if err := migration.InitializeMigrations(db, migrationFolder()); err != nil {
slog.Error("couldn't finish migration", slog.String("err", err.Error())) log.Fatalf("couldn't execute migrations: %v", err)
os.Exit(1)
} }
server := api.SetupServer(client) server := api.SetupServer(db)
port := ":" + getOrDefault("SERVER_PORT", "5281") log.Fatal(http.ListenAndServe(":5281", server))
slog.Info("app is ready", slog.String("port", port))
if err := http.ListenAndServe(port, server); err != nil {
slog.Error("Couldn't start server!\n", slog.Any("err", err.Error()))
}
} }
func hasProfile(value string) bool { func hasProfile(value string) bool {
@ -64,26 +49,3 @@ func migrationFolder() fs.FS {
} }
return db.ProdMigrations return db.ProdMigrations
} }
func buildInfo() (string, string) {
revision := ""
time := ""
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
revision = setting.Value
} else if setting.Key == "vcs.time" {
time = setting.Value
}
}
}
return revision, time
}
func getOrDefault(env string, defaultValue string) string {
if value, present := os.LookupEnv(env); present {
return value
}
return defaultValue
}

View File

@ -1,11 +1,19 @@
# scripts for building app # scripts for building app
# requires go 1.22+ and git installed # requires go 1.19+ and git installed
VERSION := $(shell git describe --tags --always) VERSION := 1.0.0
serve:
go run ./...
setup:
go get
docker-dev: docker-dev:
docker image build -t registry.bbr-dev.info/holiday-api/backend/dev:latest . docker image build -t registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev .
docker image push registry.bbr-dev.info/holiday-api/backend/dev:latest docker tag registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev registry.bbr-dev.info/holiday-api/backend:latest-dev
docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev
docker image push registry.bbr-dev.info/holiday-api/backend:latest-dev
docker-prod: docker-prod:
@ -14,6 +22,10 @@ docker-prod:
docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION) docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)
docker image push registry.bbr-dev.info/holiday-api/backend:latest docker image push registry.bbr-dev.info/holiday-api/backend:latest
release:
git tag $(VERSION)
git push origin $(VERSION)
test: test:
go test ./... go test ./...

View File

@ -4,11 +4,10 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"io/fs" "io/fs"
"log/slog" "log"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -93,8 +92,7 @@ func validateMigrations(db *sqlx.DB, migrations map[string]Migration, migrationF
} }
func executeMigration(db *sqlx.DB, name string, script string) error { func executeMigration(db *sqlx.DB, name string, script string) error {
logger := slog.Default().With(slog.String("script", name)) log.Printf("[INFO] script='%s' | migrations - executing", name)
logger.Info("migrations - executing")
tx := db.MustBeginTx(context.Background(), nil) tx := db.MustBeginTx(context.Background(), nil)
var err error = nil var err error = nil
if _, e := tx.Exec(script); e != nil { if _, e := tx.Exec(script); e != nil {
@ -104,10 +102,10 @@ func executeMigration(db *sqlx.DB, name string, script string) error {
err = e err = e
} }
if err != nil { if err != nil {
logger.Error("migrations - failed executing", slog.String("err", err.Error())) log.Printf("[ERROR] script='%s' | migrations - failed executing", name)
tx.Rollback() tx.Rollback()
} else { } else {
logger.Info("migrations - successfully executed") log.Printf("[INFO] script='%s' | migrations - succesfully executed", name)
tx.Commit() tx.Commit()
} }
return err return err
@ -121,9 +119,9 @@ func validateMigration(name string, migration Migration, script string) error {
calculatedHash := hash(script) calculatedHash := hash(script)
if calculatedHash != migration.Hash { if calculatedHash != migration.Hash {
err := errors.New(fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash)) err := fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash)
slog.Error("migrations - failed validation", slog.String("script", name), slog.String("err", err.Error())) log.Printf("[ERROR] script='%s' err='%s' | migrations - failed executing", script, err)
return err return fmt.Errorf("migrations - mismatch in hashes for %s", name)
} }
return nil return nil
} }