diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 824714cbdd829bf144585a24277bc109bd28a82d..45332347fd56d1f809fef2079af45fea318b47b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,18 @@ include: # See CGO_ENABLED issue: https://github.com/golang/go/issues/27303 test: stage: test + services: + - docker:dind + - postgres:14.1 + variables: + # Improve performance with overlayfs. + DOCKER_DRIVER: overlay2 + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_DB: testdb + POSTGRES_USER: sa + POSTGRES_PASSWORD: sa + POSTGRES_EMBEDDED: false script: - CGO_ENABLED=0 go test ./... -coverprofile=coverage-report.out - go tool cover -html=coverage-report.out -o coverage-report.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b8cafc9b6fe4d536135075bdc533acf4005ee09..a705f773de3796c9bb0c1dbad5a4c39baf7875b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,12 @@ ### Running Locally +1. Set up PostgreSQL database + 1. You can use an embedded database and set an env variable `POSTGRES_EMBEDDED` to `true`. + 2. Or start a database using Docker -> [Docker services](https://gitlab.com/ShowMeYourCodeYouTube/docker-services). + 1. Change the env variable `POSTGRES_EMBEDDED` to `false`. +2. Run the app. + ``` go run main.go ``` @@ -49,3 +55,8 @@ Ref: https://golang.cafe/blog/how-to-upgrade-golang-dependencies.html go get example.com/pkg@none ``` + + +## Other Go examples + +- https://github.com/ruyjfs/example-golang diff --git a/README.md b/README.md index 14f41e3dc03bd831dd16d9190804de570b188d21..14cc6be626e5a59996f03b54c96437ba30dc7742 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,20 @@ - stretchr/testify - logrus v1.81 - Go linter -- PostgreSQL (todo) +- RESTful API +- PostgreSQL 14 (be sure that related packages support this version) + - embedded PostgreSQL + - useful if no Docker is installed +- GORM + - GORM PostgreSQL driver - GraphQL (todo) -- gorm (todo) -- upperdb (todo) -- sqlx (todo) -- mockery (todo) - protobuf (todo) - Prometheus metrics +- stretchr/testify +- mockery (todo) +--- + +PostgreSQL local setup - [Docker services](https://gitlab.com/ShowMeYourCodeYouTube/docker-services) ## Project links diff --git a/database/postgresql.go b/database/postgresql.go new file mode 100644 index 0000000000000000000000000000000000000000..2f51fc4327e37b00d654c9b11f298a1363d17373 --- /dev/null +++ b/database/postgresql.go @@ -0,0 +1,166 @@ +package database + +import ( + "encoding/json" + "fmt" + "github.com/sirupsen/logrus" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "log" + "os" + "showmeyourcode/go/demo/model/playlist" + "time" +) + +// Playlists +// CreatedAt and UpdatedAt are special fields that GORM automatically +// populates with the current time when a record is created or updated. +// https://gorm.io/docs/models.html +type Playlists struct { + gorm.Model + UserId string `gorm:"type:varchar(36);primaryKey"` + Data string `gorm:"type:jsonb;not null"` + CustomCreatedAt time.Time `gorm:"datetime:timestamp;default:CURRENT_TIMESTAMP;not null"` +} + +func (p *Playlists) ToPlaylist() *playlist.Playlist { + var item playlist.Playlist + err := json.Unmarshal([]byte(p.Data), &item) + if err != nil { + logrus.Errorf("Cannot deserialize items for %v. Error: %v", p.UserId, err) + } + return &item +} + +var dbInstance *gorm.DB + +func GetHost() string { + return GetEnv("POSTGRES_HOST", "localhost") +} + +func GetPort() string { + return GetEnv("POSTGRES_PORT", "5432") +} + +func GetDatabaseName() string { + return GetEnv("POSTGRES_DB", "localdevelopment") +} + +func GetUser() string { + return GetEnv("POSTGRES_USER", "root") +} + +func GetPassword() string { + return GetEnv("POSTGRES_PASSWORD", "root") +} + +func GetTimezone() string { + return GetEnv("POSTGRES_TIMEZONE", "UTC") +} + +func Db() *gorm.DB { + if dbInstance == nil { + logrus.Info("Initializing db connection...") + // local setup based on https://gitlab.com/ShowMeYourCodeYouTube/docker-services + + dsn := fmt.Sprintf( + "host=%s port=%s dbname=%s user=%s password=%s sslmode=disable TimeZone=%s", + GetHost(), + GetPort(), + GetDatabaseName(), + GetUser(), + GetPassword(), + GetTimezone(), + ) + + logrus.Infof("DB details - host: %v, port: %v, dbName: %v, user: %v", GetHost(), GetPort(), GetDatabaseName(), GetUser()) + + // Use SDK logger to differentiate from the standard logger only for DEMO purposes. + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // Slow SQL threshold + LogLevel: logger.Info, // Log level + IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger + ParameterizedQueries: true, // Don't include params in the SQL log + Colorful: false, // Disable color + }, + ) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: newLogger, + PrepareStmt: true, + }) + if err != nil { + panic("failed to connect database") + } + + dbInstance = db + + return dbInstance + } else { + logrus.Info("Reusing a DB connection.") + return dbInstance + } +} + +func MigrateDatabase() { + logrus.Info("Database migration: Start") + db := Db() + err := db.AutoMigrate( + &Playlists{}, + ) + if err != nil { + panic("failed to migrate database") + } + + logrus.Info("Database migration: Success") + seed(db) +} + +func seed(db *gorm.DB) { + logrus.Info("Add default database entries if empty") + var playlists []Playlists + var count int64 + db.Model(&playlists).Count(&count) + logrus.Infof("Current playlist count: %v", count) + + if count == 0 { + playlists = []Playlists{ + {UserId: "f1a9242a-120c-40c6-9981-51916009d0b3", + Data: toString(&playlist.Playlist{ + Id: "bcd94fd8-ca5f-42f2-8d11-691faff8e46d", + Items: []playlist.PlaylistItem{{Name: "Item1", LastModifiedAt: time.Now().UnixNano()}, {Name: "Item2", LastModifiedAt: time.Now().UnixNano()}}, + }), + }, + {UserId: "c3dce4af-1a7d-4da7-aded-d1cde63062ae", + Data: toString(&playlist.Playlist{ + Id: "c3dce4af-1a7d-4da7-aded-d1cde63062ae", + Items: []playlist.PlaylistItem{}, + }), + }, + } + db.Create(&playlists) + } else { + logrus.Warn("Database is not empty. Not adding default records.") + } +} + +func toString(playlist *playlist.Playlist) string { + bytes, err := json.Marshal(playlist) + + if err != nil { + panic(err) + } + + return string(bytes) +} + +func GetEnv(key, defaultValue string) string { + value := os.Getenv(key) + if len(value) == 0 { + return defaultValue + } + return value +} diff --git a/database/postgresql_embedded.go b/database/postgresql_embedded.go new file mode 100644 index 0000000000000000000000000000000000000000..36044c9f4204ac89523b4d8add42b3c312bbd697 --- /dev/null +++ b/database/postgresql_embedded.go @@ -0,0 +1,31 @@ +package database + +import ( + "bytes" + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + log "github.com/sirupsen/logrus" + "strconv" + "time" +) + +func StartEmbeddedDatabase() { + isEnabled := GetEnv("POSTGRES_EMBEDDED", "false") == "true" + if isEnabled { + log.Info("Starting embedded database...") + logger := &bytes.Buffer{} + port, _ := strconv.Atoi(GetPort()) + postgres := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig(). + Username(GetUser()). + Password(GetPassword()). + Database(GetDatabaseName()). + Version(embeddedpostgres.V14). + RuntimePath("/tmp"). + BinaryRepositoryURL("https://repo1.maven.org/maven2"). + Port(uint32(port)). + StartTimeout(15 * time.Second). + StartParameters(map[string]string{"max_connections": "200"}). + Logger(logger)) + err := postgres.Start() + panic(err) + } +} diff --git a/docs/docs.go b/docs/docs.go index e51c4c892201d2737dfd6970f044b650c2d43586..66e593ab58842878ed4f56caae0cf37162882a9d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -39,7 +39,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/service.PlaylistResponse" + "$ref": "#/definitions/playlist.PlaylistResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/web.Error" } }, "404": { @@ -72,13 +78,16 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/service.NewPlaylistRequest" + "$ref": "#/definitions/playlist.NewPlaylistRequest" } } ], "responses": { "201": { - "description": "Created" + "description": "Created", + "schema": { + "$ref": "#/definitions/playlist.Playlist" + } }, "400": { "description": "Bad Request", @@ -160,7 +169,7 @@ const docTemplate = `{ } } }, - "service.NewPlaylist": { + "playlist.NewPlaylist": { "type": "object", "properties": { "id": { @@ -169,12 +178,12 @@ const docTemplate = `{ "items": { "type": "array", "items": { - "$ref": "#/definitions/service.NewPlaylistItem" + "$ref": "#/definitions/playlist.NewPlaylistItem" } } } }, - "service.NewPlaylistItem": { + "playlist.NewPlaylistItem": { "type": "object", "properties": { "lastModifiedAt": { @@ -185,7 +194,7 @@ const docTemplate = `{ } } }, - "service.NewPlaylistRequest": { + "playlist.NewPlaylistRequest": { "type": "object", "required": [ "playlists", @@ -193,14 +202,14 @@ const docTemplate = `{ ], "properties": { "playlists": { - "$ref": "#/definitions/service.NewPlaylist" + "$ref": "#/definitions/playlist.NewPlaylist" }, "userId": { "type": "string" } } }, - "service.Playlist": { + "playlist.Playlist": { "type": "object", "properties": { "id": { @@ -209,12 +218,12 @@ const docTemplate = `{ "items": { "type": "array", "items": { - "$ref": "#/definitions/service.PlaylistItem" + "$ref": "#/definitions/playlist.PlaylistItem" } } } }, - "service.PlaylistItem": { + "playlist.PlaylistItem": { "type": "object", "properties": { "lastModifiedAt": { @@ -225,13 +234,13 @@ const docTemplate = `{ } } }, - "service.PlaylistResponse": { + "playlist.PlaylistResponse": { "type": "object", "properties": { "playlists": { "type": "array", "items": { - "$ref": "#/definitions/service.Playlist" + "$ref": "#/definitions/playlist.Playlist" } }, "userId": { diff --git a/docs/swagger.json b/docs/swagger.json index f4df81373d63ecc98dd0afcd8c9958d7d932231d..a3cef382e99fa9d5ad990c884c014eee181d6a04 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -27,7 +27,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/service.PlaylistResponse" + "$ref": "#/definitions/playlist.PlaylistResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/web.Error" } }, "404": { @@ -60,13 +66,16 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/service.NewPlaylistRequest" + "$ref": "#/definitions/playlist.NewPlaylistRequest" } } ], "responses": { "201": { - "description": "Created" + "description": "Created", + "schema": { + "$ref": "#/definitions/playlist.Playlist" + } }, "400": { "description": "Bad Request", @@ -148,7 +157,7 @@ } } }, - "service.NewPlaylist": { + "playlist.NewPlaylist": { "type": "object", "properties": { "id": { @@ -157,12 +166,12 @@ "items": { "type": "array", "items": { - "$ref": "#/definitions/service.NewPlaylistItem" + "$ref": "#/definitions/playlist.NewPlaylistItem" } } } }, - "service.NewPlaylistItem": { + "playlist.NewPlaylistItem": { "type": "object", "properties": { "lastModifiedAt": { @@ -173,7 +182,7 @@ } } }, - "service.NewPlaylistRequest": { + "playlist.NewPlaylistRequest": { "type": "object", "required": [ "playlists", @@ -181,14 +190,14 @@ ], "properties": { "playlists": { - "$ref": "#/definitions/service.NewPlaylist" + "$ref": "#/definitions/playlist.NewPlaylist" }, "userId": { "type": "string" } } }, - "service.Playlist": { + "playlist.Playlist": { "type": "object", "properties": { "id": { @@ -197,12 +206,12 @@ "items": { "type": "array", "items": { - "$ref": "#/definitions/service.PlaylistItem" + "$ref": "#/definitions/playlist.PlaylistItem" } } } }, - "service.PlaylistItem": { + "playlist.PlaylistItem": { "type": "object", "properties": { "lastModifiedAt": { @@ -213,13 +222,13 @@ } } }, - "service.PlaylistResponse": { + "playlist.PlaylistResponse": { "type": "object", "properties": { "playlists": { "type": "array", "items": { - "$ref": "#/definitions/service.Playlist" + "$ref": "#/definitions/playlist.Playlist" } }, "userId": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 810c89f259a85fe7296e9d19895677918d6ce1fd..2141fd213a7ac252c3e9d79ddc51bac69a35f1df 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -17,53 +17,53 @@ definitions: version: type: string type: object - service.NewPlaylist: + playlist.NewPlaylist: properties: id: type: string items: items: - $ref: '#/definitions/service.NewPlaylistItem' + $ref: '#/definitions/playlist.NewPlaylistItem' type: array type: object - service.NewPlaylistItem: + playlist.NewPlaylistItem: properties: lastModifiedAt: type: integer name: type: string type: object - service.NewPlaylistRequest: + playlist.NewPlaylistRequest: properties: playlists: - $ref: '#/definitions/service.NewPlaylist' + $ref: '#/definitions/playlist.NewPlaylist' userId: type: string required: - playlists - userId type: object - service.Playlist: + playlist.Playlist: properties: id: type: string items: items: - $ref: '#/definitions/service.PlaylistItem' + $ref: '#/definitions/playlist.PlaylistItem' type: array type: object - service.PlaylistItem: + playlist.PlaylistItem: properties: lastModifiedAt: type: integer name: type: string type: object - service.PlaylistResponse: + playlist.PlaylistResponse: properties: playlists: items: - $ref: '#/definitions/service.Playlist' + $ref: '#/definitions/playlist.Playlist' type: array userId: type: string @@ -99,7 +99,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/service.PlaylistResponse' + $ref: '#/definitions/playlist.PlaylistResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/web.Error' "404": description: Not found schema: @@ -119,12 +123,14 @@ paths: name: playlistRequest required: true schema: - $ref: '#/definitions/service.NewPlaylistRequest' + $ref: '#/definitions/playlist.NewPlaylistRequest' produces: - application/json responses: "201": description: Created + schema: + $ref: '#/definitions/playlist.Playlist' "400": description: Bad Request schema: diff --git a/go.mod b/go.mod index 7814f60ae0f9298c2c3a2e622f66ffa916550c85..892c587ffbee125de48f4dccd437e304bdd4e455 100644 --- a/go.mod +++ b/go.mod @@ -12,18 +12,21 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.13 // indirect github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.1 github.com/swaggo/gin-swagger v1.3.0 github.com/swaggo/swag v1.8.1 - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/crypto v0.20.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/text v0.14.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( + github.com/fergusstrange/embedded-postgres v1.26.0 github.com/google/uuid v1.1.2 github.com/prometheus/client_golang v1.19.0 + gorm.io/driver/postgres v1.5.7 + gorm.io/gorm v1.25.10 ) require ( @@ -36,8 +39,14 @@ require ( github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect github.com/golang/protobuf v1.5.3 // 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.4.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -45,7 +54,8 @@ require ( github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/ugorji/go/codec v1.2.6 // indirect - golang.org/x/sys v0.16.0 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 27929fcbc1baa9db4dd31bc66623a64a433f7174..15a76033f9a89d069e1f0d22cd9e7daab6560640 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fergusstrange/embedded-postgres v1.26.0 h1:mTgUBNST+6zro0TkIb9Fuo9Qg8mSU0ILus9jZKmFmJg= +github.com/fergusstrange/embedded-postgres v1.26.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= @@ -63,6 +65,16 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -80,6 +92,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -118,12 +132,17 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= github.com/swaggo/gin-swagger v1.3.0 h1:eOmp7r57oUgZPw2dJOjcGNMse9cvXcI4tTqBcnZtPsI= github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0m5SkWx+cS0= @@ -140,11 +159,15 @@ github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCB github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -155,8 +178,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -170,8 +193,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -204,5 +227,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/model/playlist/playlist.go b/model/playlist/playlist.go new file mode 100644 index 0000000000000000000000000000000000000000..1b15595c149237ccd5cbb1391cdc056b501ee8da --- /dev/null +++ b/model/playlist/playlist.go @@ -0,0 +1,37 @@ +package playlist + +import "errors" + +type PlaylistItem struct { + Name string `json:"name,omitempty"` + LastModifiedAt int64 `json:"lastModifiedAt,omitempty"` +} + +type Playlist struct { + Id string `json:"id,omitempty"` + Items []PlaylistItem `json:"items,omitempty"` +} + +type PlaylistResponse struct { + UserId string `json:"userId,omitempty"` + Playlist []Playlist `json:"playlists,omitempty"` +} + +var ErrNotFound = errors.New("resource was not found") + +// Create a playlist + +type NewPlaylistRequest struct { + UserId string `json:"userId,omitempty" binding:"required"` + Playlist NewPlaylist `json:"playlists,omitempty" binding:"required"` +} + +type NewPlaylist struct { + Id string `json:"id,omitempty"` + Items []NewPlaylistItem `json:"items,omitempty"` +} + +type NewPlaylistItem struct { + Name string `json:"name,omitempty"` + LastModifiedAt int64 `json:"lastModifiedAt,omitempty"` +} diff --git a/router/api/v1/playlists.go b/router/api/v1/playlists.go index 6397710eeca68c48a9682e14c8c8789ff086f8c0..b2395b59e443f44f9bdb707e83bc52d6c65e10de 100644 --- a/router/api/v1/playlists.go +++ b/router/api/v1/playlists.go @@ -4,8 +4,10 @@ import ( "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "net/http" + "showmeyourcode/go/demo/model/playlist" "showmeyourcode/go/demo/model/web" "showmeyourcode/go/demo/service" + "strings" ) // @Summary Get playlists @@ -13,7 +15,8 @@ import ( // @Tags v1 // @Produce json // @Param userId query string true "ID that identifies a user." -// @Success 200 {object} service.PlaylistResponse +// @Success 200 {object} playlist.PlaylistResponse +// @Failure 400 {object} web.Error "Bad request" // @Failure 404 {object} web.Error "Not found" // @Failure 500 {object} web.Error "Internal_server_error" // @Router /api/v1/playlists [get] @@ -21,9 +24,14 @@ func GetUserPlaylists(context *gin.Context) { log.Info("Getting user playlists...") userId := context.Query("userId") + if len(strings.Trim(userId, " ")) == 0 { + web.WriteErrorResponse(context, "Validation failed. 'userId' cannot be empty.", http.StatusBadRequest) + return + } + playlistListResponse, err := service.GetPlaylist(userId) if err != nil { - if err == service.ErrNotFound { + if err == playlist.ErrNotFound { web.WriteErrorResponse(context, err.Error(), http.StatusNotFound) } else { web.WriteErrorResponse(context, err.Error(), http.StatusInternalServerError) @@ -37,24 +45,29 @@ func GetUserPlaylists(context *gin.Context) { // @Description Add or override user's playlist // @Tags v1 // @Produce json -// @Param playlistRequest body service.NewPlaylistRequest true "Playlist body" -// @Success 201 "Created" +// @Param playlistRequest body playlist.NewPlaylistRequest true "Playlist body" +// @Success 201 {object} playlist.Playlist "Created" // @Failure 400 {object} web.Error "Bad Request" // @Failure 500 {object} web.Error "Internal_server_error" // @Router /api/v1/playlists [post] func AddUserPlaylists(context *gin.Context) { log.Info("Add user playlists...") - var json service.NewPlaylistRequest + var json playlist.NewPlaylistRequest if err := context.ShouldBindJSON(&json); err != nil { web.WriteErrorResponse(context, err.Error(), http.StatusBadRequest) return } - service.AddPlaylist(json) + result, err := service.AddPlaylist(json) + + if err != nil { + web.WriteErrorResponse(context, err.Error(), http.StatusInternalServerError) + return + } log.Infof("Playlists created for %v", json.UserId) - web.WriteJsonResponse(context, web.GenerateServerSuccessfulResponse(http.StatusCreated), http.StatusCreated) + web.WriteJsonResponse(context, result, http.StatusCreated) } diff --git a/router/api/v1/playlists_test.go b/router/api/v1/playlists_test.go index 540f965f2e170d80181015232fdbab880314bc54..818f55d166a8fc77b67b320505b016499b8c75a0 100644 --- a/router/api/v1/playlists_test.go +++ b/router/api/v1/playlists_test.go @@ -6,7 +6,7 @@ import ( "net/http" "net/http/httptest" "showmeyourcode/go/demo/constant/path" - "showmeyourcode/go/demo/service" + "showmeyourcode/go/demo/model/playlist" "showmeyourcode/go/demo/test" "strings" "testing" @@ -28,7 +28,7 @@ func TestGetPlaylistsReturnPlaylistsWhenUserIdIsValid(t *testing.T) { assert.Equal(t, http.StatusOK, responseRecorder.Code) assert.NotNil(t, responseRecorder.Body) - response := &service.PlaylistResponse{} + response := &playlist.PlaylistResponse{} if err := json.Unmarshal(responseRecorder.Body.Bytes(), response); err != nil { panic(err) @@ -52,7 +52,7 @@ func TestGetPlaylistsReturnErrorWhenUserIdIsMissing(t *testing.T) { router.ServeHTTP(responseRecorder, req) // THEN - assert.Equal(t, http.StatusNotFound, responseRecorder.Code) + assert.Equal(t, http.StatusBadRequest, responseRecorder.Code) assert.NotNil(t, responseRecorder.Body) } @@ -79,11 +79,11 @@ func TestGetPlaylistsReturnErrorWhenUserDoesNotExist(t *testing.T) { // ==== func TestCreateNewPlaylistWhenRequestIsValid(t *testing.T) { // GIVEN - payload := service.NewPlaylistRequest{ + payload := playlist.NewPlaylistRequest{ UserId: "f1a9242a-120c-40c6-9981-51916009d0b3", - Playlist: service.NewPlaylist{ + Playlist: playlist.NewPlaylist{ Id: "id1", - Items: []service.NewPlaylistItem{{Name: "test1", LastModifiedAt: 123}}, + Items: []playlist.NewPlaylistItem{{Name: "test1", LastModifiedAt: 123}}, }, } payloadAsBytes, _ := json.Marshal(payload) diff --git a/service/initialization.go b/service/initialization.go index 065ebcc916c8a846216a70c63e8f326f86a962e8..82fb4d619f74265bb2d1ba4806e8fe1d4b5cf995 100644 --- a/service/initialization.go +++ b/service/initialization.go @@ -1,7 +1,11 @@ package service +import "showmeyourcode/go/demo/database" + func InitializeApplication() { SwaggerConfiguration() SetServerMode() SetLogLevel() + database.StartEmbeddedDatabase() + database.MigrateDatabase() } diff --git a/service/playlist.go b/service/playlist.go index ccf214a2150efcad977bf2def68969ba27334dda..7d2a1da0d88016428cd96bd43cb78df5d165ead1 100644 --- a/service/playlist.go +++ b/service/playlist.go @@ -1,90 +1,68 @@ package service import ( + "encoding/json" "errors" + "fmt" log "github.com/sirupsen/logrus" - "time" + "showmeyourcode/go/demo/database" + "showmeyourcode/go/demo/model/playlist" ) -var playlists = map[string]Playlist{ - "f1a9242a-120c-40c6-9981-51916009d0b3": { - Id: "bcd94fd8-ca5f-42f2-8d11-691faff8e46d", - Items: []PlaylistItem{{Name: "Item1", LastModifiedAt: time.Now().UnixNano()}, {Name: "Item2", LastModifiedAt: time.Now().UnixNano()}}, - }, - "3741c1be-3129-4ee9-83e0-602f70038c1c": { - Id: "c3dce4af-1a7d-4da7-aded-d1cde63062ae", - Items: []PlaylistItem{}, - }, -} - -type PlaylistItem struct { - Name string `json:"name,omitempty"` - LastModifiedAt int64 `json:"lastModifiedAt,omitempty"` -} - -type Playlist struct { - Id string `json:"id,omitempty"` - Items []PlaylistItem `json:"items,omitempty"` -} - -type PlaylistResponse struct { - UserId string `json:"userId,omitempty"` - Playlist []Playlist `json:"playlists,omitempty"` -} - -var ErrNotFound = errors.New("resource was not found") - -// Create a playlist - -type NewPlaylistRequest struct { - UserId string `json:"userId,omitempty" binding:"required"` - Playlist NewPlaylist `json:"playlists,omitempty" binding:"required"` -} +func GetPlaylist( + userId string, +) (pr *playlist.PlaylistResponse, e error) { + + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered. Error:\n", r) + switch v := r.(type) { + case error: + e = v + default: + e = errors.New("unexpected error occurred during processing a request") + } + } + }() + + dbPlaylist := database.Playlists{} + + result := database.Db().First(&dbPlaylist, database.Playlists{UserId: userId}) + dbError := result.Error + if dbError != nil { + log.Errorf("Unexupected error occurred while processing a db request for %v. Error: %v", userId, dbError) + if dbError.Error() == "record not found" { + return nil, playlist.ErrNotFound + } else { + return nil, dbError + } + } -type NewPlaylist struct { - Id string `json:"id,omitempty"` - Items []NewPlaylistItem `json:"items,omitempty"` + log.Infof("Query details - userId: %v, rowsAffected: %v, result: %v", userId, result.RowsAffected, dbPlaylist) + return &playlist.PlaylistResponse{UserId: userId, Playlist: []playlist.Playlist{*dbPlaylist.ToPlaylist()}}, nil } -type NewPlaylistItem struct { - Name string `json:"name,omitempty"` - LastModifiedAt int64 `json:"lastModifiedAt,omitempty"` -} +func AddPlaylist(request playlist.NewPlaylistRequest) (*playlist.Playlist, error) { + data, err := json.Marshal(request.Playlist) -func (npi *NewPlaylistItem) toPlaylistItem() PlaylistItem { - return PlaylistItem{ - Name: npi.Name, - LastModifiedAt: npi.LastModifiedAt, + if err != nil { + log.Errorf("Unexupected error occurred while processing the request. Error: %v", err) + return nil, err } -} -func (np *NewPlaylist) toPlaylist() Playlist { - items := make([]PlaylistItem, len(np.Items)) - for idx, item := range np.Items { - items[idx] = item.toPlaylistItem() + dbEntity := database.Playlists{ + UserId: request.UserId, + Data: string(data), } - return Playlist{ - Id: np.Id, - Items: items, - } -} + result := database.Db().Create(&dbEntity) -func GetPlaylist( - clientId string, -) (*PlaylistResponse, error) { + log.Infof("Query details - rowsAffected: %v, ", result.RowsAffected) + dbError := result.Error - // no need to synchronize as it's readonly - v, exist := playlists[clientId] - - if exist { - log.Infof("Fetched playlists for: %v", clientId) - return &PlaylistResponse{UserId: clientId, Playlist: []Playlist{v}}, nil - } else { - log.Errorf("Cannot fetch playlists for %v!", clientId) - return nil, ErrNotFound + if dbError != nil { + log.Errorf("Cannot save a playlist. Error: %v", dbError) + return nil, dbError } -} -func AddPlaylist(request NewPlaylistRequest) { - playlists[request.UserId] = request.Playlist.toPlaylist() + return dbEntity.ToPlaylist(), nil }