From 852c181ac336371152017db39ab7922dd8f6de2d Mon Sep 17 00:00:00 2001 From: Piotr Rogowski Date: Sun, 6 Nov 2022 15:12:45 +0100 Subject: [PATCH] Add stargazers endpoint (#1) --- .gitignore | 1 + .vscode/settings.json | 4 +- UPGRADE.md | 10 +- go.mod | 10 +- main.go | 138 ++++++++++++++++++++- migrations/1667688866_create_stargazers.go | 20 +++ pb_schema.json | 52 ++++++++ 7 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 migrations/1667688866_create_stargazers.go diff --git a/.gitignore b/.gitignore index 2b64f21..27f053a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ go.work # build cloud-backend cloud-backend.exe +main # docker overrides docker-compose.override.yml diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c5404e..9c08858 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,9 @@ { "cSpell.words": [ + "daos", "labstack", "Middlewares", - "pocketbase" + "pocketbase", + "unstar" ] } diff --git a/UPGRADE.md b/UPGRADE.md index 5f69297..3091130 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,9 +1,17 @@ # Upgrade guide +## From v1.0.1 to v1.1.0 + +This version adds stargazers. + +1. import new schema first +2. get the new binary or Dockerfile and run `cloud-backend`, make sure everything works as it was before (don't forget to backup `pb_data` data) +3. upgrade frontend to `v1.1.0` + ## From v1.0.0 to v1.0.1 This version adds to new custom endpoints for fetching tune and ini file. -1. get the new binary and run `cloud-backend`, make sure everything works as it was before (don't forget to backup `pb_data` data) +1. get the new binary or Dockerfile and run `cloud-backend`, make sure everything works as it was before (don't forget to backup `pb_data` data) 2. upgrade frontend to `v1.0.1` 3. import new schema diff --git a/go.mod b/go.mod index 9c60b99..82fa291 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,12 @@ -module hyper-tuner/cloud-backend +module main go 1.19 -require github.com/pocketbase/pocketbase v0.8.0-rc2.0.20221105112208-a2abeb872aca +require ( + github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 + github.com/pocketbase/dbx v1.7.0 + github.com/pocketbase/pocketbase v0.8.0-rc2.0.20221105112208-a2abeb872aca +) require ( github.com/AlecAivazis/survey/v2 v2.3.6 // indirect @@ -42,12 +46,10 @@ require ( github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/pocketbase/dbx v1.7.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.6.1 // indirect diff --git a/main.go b/main.go index 72fc639..51b9a4d 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,22 @@ import ( "log" "net/http" + _ "main/migrations" + "github.com/labstack/echo/v5" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" ) func main() { + tunesCollectionName := "tunes" + iniFilesCollectionName := "iniFiles" + stargazersCollectionName := "stargazers" + app := pocketbase.New() app.OnBeforeServe().Add(func(e *core.ServeEvent) error { @@ -19,14 +27,14 @@ func main() { Method: http.MethodGet, Path: "/api/custom/tunes/byTuneId/:tuneId", Handler: func(c echo.Context) error { - record, _err := app.Dao().FindFirstRecordByData("tunes", "tuneId", c.PathParam("tuneId")) + record, _err := app.Dao().FindFirstRecordByData(tunesCollectionName, "tuneId", c.PathParam("tuneId")) if _err != nil { return apis.NewNotFoundError("Tune not found", nil) } _errors := app.Dao().ExpandRecord(record, []string{"author"}, func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) { - record, _err := app.Dao().FindFirstRecordByData(relCollection.Name, "id", relIds[0]) + record, _err := app.Dao().FindRecordById(relCollection.Name, relIds[0]) return []*models.Record{record}, _err }) @@ -43,7 +51,7 @@ func main() { Method: http.MethodGet, Path: "/api/custom/iniFiles/bySignature/:signature", Handler: func(c echo.Context) error { - record, _err := app.Dao().FindFirstRecordByData("iniFiles", "signature", c.PathParam("signature")) + record, _err := app.Dao().FindFirstRecordByData(iniFilesCollectionName, "signature", c.PathParam("signature")) if _err != nil { return c.JSON(http.StatusNotFound, _err) @@ -53,6 +61,130 @@ func main() { }, }) + e.Router.AddRoute(echo.Route{ + Method: http.MethodPost, + Path: "/api/custom/stargazers/toggleStar", + Handler: func(c echo.Context) (err error) { + auth := c.Get(apis.ContextAuthRecordKey).(*models.Record) + isStarred := false + + type Stargazer struct { + User string + Tune string `json:"tune" form:"tune" query:"tune"` + } + + stargazer := new(Stargazer) + + if err = c.Bind(stargazer); err != nil { + return c.String(http.StatusBadRequest, "Bad request") + } + + stargazer.User = auth.Id + + _err := app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + collection, err := app.Dao().FindCollectionByNameOrId(stargazersCollectionName) + if err != nil { + return err + } + + stargazerRecord := models.NewRecord(collection) + stargazerRecord.Set("user", stargazer.User) + stargazerRecord.Set("tune", stargazer.Tune) + + tune, _err := app.Dao().FindFirstRecordByData(tunesCollectionName, "id", stargazer.Tune) + if _err != nil { + return apis.NewNotFoundError("Tune not found", nil) + } + + _err = txDao.SaveRecord(stargazerRecord) + + // UNIQUE constraint failed most likely, try to unstar + if _err != nil { + currentStargazerRecords, _err := app.Dao().FindRecordsByExpr( + stargazersCollectionName, + dbx.HashExp{"user": stargazer.User}, + dbx.HashExp{"tune": stargazer.Tune}, + ) + + if _err != nil || len(currentStargazerRecords) == 0 { + return _err + } + + _err = txDao.DeleteRecord(currentStargazerRecords[0]) + if _err != nil { + return _err + } + + isStarred = false + tune.Set("stars", tune.Get("stars").(float64)-1) + } else { + isStarred = true + tune.Set("stars", tune.Get("stars").(float64)+1) + } + + _err = txDao.SaveRecord(tune) + if _err != nil { + return _err + } + + return nil + }) + + if _err != nil { + return apis.NewNotFoundError("Tune not found or already starred", nil) + } + + // fetch again and return current state + tune, _err := app.Dao().FindFirstRecordByData(tunesCollectionName, "id", stargazer.Tune) + + return c.JSON(http.StatusOK, map[string]any{ + "stars": tune.Get("stars").(float64), + "isStarred": isStarred, + }) + }, + Middlewares: []echo.MiddlewareFunc{ + apis.LoadAuthContext(app.App), + apis.RequireAdminOrRecordAuth("users"), + }, + }) + + e.Router.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/api/custom/stargazers/starredByMe/:tune", + Handler: func(c echo.Context) (err error) { + auth := c.Get(apis.ContextAuthRecordKey).(*models.Record) + isStarred := true + + type Stargazer struct { + User string + Tune string + } + + stargazer := new(Stargazer) + + stargazer.User = auth.Id + stargazer.Tune = c.PathParam("tune") + + stargazerRecords, _err := app.Dao().FindRecordsByExpr( + stargazersCollectionName, + dbx.HashExp{"user": stargazer.User}, + dbx.HashExp{"tune": stargazer.Tune}, + ) + + if _err != nil || len(stargazerRecords) == 0 { + isStarred = false + } + + return c.JSON(http.StatusOK, map[string]any{ + "isStarred": isStarred, + }) + }, + Middlewares: []echo.MiddlewareFunc{ + apis.LoadAuthContext(app.App), + apis.RequireAdminOrRecordAuth("users"), + }, + }) + return nil }) diff --git a/migrations/1667688866_create_stargazers.go b/migrations/1667688866_create_stargazers.go new file mode 100644 index 0000000..8f6efd0 --- /dev/null +++ b/migrations/1667688866_create_stargazers.go @@ -0,0 +1,20 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(db dbx.Builder) error { + // add up queries... + db.CreateUniqueIndex("stargazers", "unique_stargazers_on_user_tune", "user", "tune").Execute() + + return nil + }, func(db dbx.Builder) error { + // add down queries... + db.DropIndex("stargazers", "unique_stargazers_on_user_tune").Execute() + + return nil + }) +} diff --git a/pb_schema.json b/pb_schema.json index 156fb99..2aebef5 100644 --- a/pb_schema.json +++ b/pb_schema.json @@ -44,6 +44,18 @@ "pattern": "" } }, + { + "id": "lwbwtgmx", + "name": "stars", + "type": "number", + "system": false, + "required": false, + "unique": false, + "options": { + "min": 0, + "max": null + } + }, { "id": "g9b17t9y", "name": "vehicleName", @@ -427,5 +439,45 @@ "onlyEmailDomains": null, "requireEmail": true } + }, + { + "id": "z8cojwcvlyxxyll", + "name": "stargazers", + "type": "base", + "system": false, + "schema": [ + { + "id": "him7pbq2", + "name": "user", + "type": "relation", + "system": false, + "required": true, + "unique": false, + "options": { + "maxSelect": 1, + "collectionId": "systemprofiles0", + "cascadeDelete": false + } + }, + { + "id": "ny7akrmn", + "name": "tune", + "type": "relation", + "system": false, + "required": true, + "unique": false, + "options": { + "maxSelect": 1, + "collectionId": "5djmpehuiigg06b", + "cascadeDelete": false + } + } + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} } ]