2020-04-27 22:43:42 +02:00
|
|
|
package route
|
|
|
|
|
|
|
|
import (
|
2020-05-03 18:34:44 +02:00
|
|
|
"encoding/json"
|
2020-04-27 22:43:42 +02:00
|
|
|
"log"
|
|
|
|
"net/http"
|
2020-05-03 18:34:44 +02:00
|
|
|
"strconv"
|
2020-04-27 22:43:42 +02:00
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
|
|
|
|
|
|
|
"forge.cadoles.com/wpetit/guesstimate/internal/model"
|
|
|
|
"forge.cadoles.com/wpetit/guesstimate/internal/storm"
|
|
|
|
"github.com/go-chi/chi"
|
2020-04-27 22:43:42 +02:00
|
|
|
"github.com/pkg/errors"
|
2020-05-03 18:34:44 +02:00
|
|
|
"gitlab.com/wpetit/goweb/middleware/container"
|
2020-04-27 22:43:42 +02:00
|
|
|
)
|
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
type projectResponse struct {
|
|
|
|
Version uint64 `json:"version"`
|
|
|
|
Project *model.Project `json:"project"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleGetProject(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctn := container.Must(r.Context())
|
|
|
|
db := storm.Must(ctn)
|
|
|
|
|
|
|
|
projectID := getProjectID(r)
|
|
|
|
|
|
|
|
var (
|
|
|
|
version uint64
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
rawVersion := r.URL.Query().Get("version")
|
|
|
|
if rawVersion != "" {
|
|
|
|
version, err = strconv.ParseUint(rawVersion, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
2020-04-27 22:43:42 +02:00
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2020-04-27 22:43:42 +02:00
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
tx, err := db.Begin(false)
|
2020-04-27 22:43:42 +02:00
|
|
|
if err != nil {
|
2020-05-03 18:34:44 +02:00
|
|
|
panic(errors.Wrap(err, "could not start transaction"))
|
2020-04-27 22:43:42 +02:00
|
|
|
}
|
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
defer func() {
|
|
|
|
if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
|
|
|
|
panic(errors.Wrap(err, "could not rollback transaction"))
|
|
|
|
}
|
|
|
|
}()
|
2020-04-27 22:43:42 +02:00
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
entry := &model.ProjectEntry{}
|
|
|
|
if err := tx.One("ID", projectID, entry); err != nil {
|
|
|
|
if err == storm.ErrNotFound {
|
|
|
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
2020-04-27 22:43:42 +02:00
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
panic(errors.Wrapf(err, "could not find project '%s'", projectID))
|
|
|
|
}
|
|
|
|
|
|
|
|
if rawVersion != "" && entry.Version == version {
|
|
|
|
http.Error(w, http.StatusText(http.StatusNotModified), http.StatusNotModified)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := writeJSON(w, http.StatusOK, &projectResponse{entry.Version, entry.Project}); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not write json"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type createRequest struct {
|
|
|
|
Project *model.Project `json:"project"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleCreateProject(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctn := container.Must(r.Context())
|
|
|
|
db := storm.Must(ctn)
|
|
|
|
|
|
|
|
projectID := getProjectID(r)
|
|
|
|
|
|
|
|
log.Printf("handling create request for project %s", projectID)
|
|
|
|
|
|
|
|
createReq := &createRequest{}
|
|
|
|
if err := parseJSONBody(r, createReq); err != nil {
|
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
|
|
|
|
|
|
panic(errors.Wrap(err, "could not parse create request"))
|
|
|
|
}
|
|
|
|
|
|
|
|
tx, err := db.Begin(true)
|
|
|
|
if err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not start transaction"))
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
|
|
|
|
panic(errors.Wrap(err, "could not rollback transaction"))
|
2020-04-27 22:43:42 +02:00
|
|
|
}
|
2020-05-03 18:34:44 +02:00
|
|
|
}()
|
|
|
|
|
|
|
|
entry := &model.ProjectEntry{}
|
|
|
|
|
|
|
|
err = tx.One("ID", projectID, entry)
|
|
|
|
if err == nil {
|
|
|
|
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != storm.ErrNotFound {
|
|
|
|
panic(errors.Wrapf(err, "could not check project '%s'", projectID))
|
|
|
|
}
|
|
|
|
|
|
|
|
entry.ID = projectID
|
|
|
|
entry.Project = createReq.Project
|
|
|
|
entry.Version = 0
|
|
|
|
|
|
|
|
if err := tx.Save(entry); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not save project"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not commit transaction"))
|
|
|
|
}
|
2020-04-27 22:43:42 +02:00
|
|
|
|
2020-05-03 18:34:44 +02:00
|
|
|
if err := writeJSON(w, http.StatusCreated, &projectResponse{entry.Version, entry.Project}); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not write json response"))
|
2020-04-27 22:43:42 +02:00
|
|
|
}
|
|
|
|
}
|
2020-05-03 18:34:44 +02:00
|
|
|
|
|
|
|
type patchRequest struct {
|
|
|
|
Version uint64 `json:"version"`
|
|
|
|
Patch json.RawMessage `json:"patch"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func handlePatchProject(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctn := container.Must(r.Context())
|
|
|
|
db := storm.Must(ctn)
|
|
|
|
|
|
|
|
projectID := getProjectID(r)
|
|
|
|
|
|
|
|
log.Printf("handling patch request for project %s", projectID)
|
|
|
|
|
|
|
|
patchReq := &patchRequest{}
|
|
|
|
if err := parseJSONBody(r, patchReq); err != nil {
|
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
|
|
|
|
|
|
panic(errors.Wrap(err, "could not parse patch request"))
|
|
|
|
}
|
|
|
|
|
|
|
|
tx, err := db.Begin(true)
|
|
|
|
if err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not start transaction"))
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
|
|
|
|
panic(errors.Wrap(err, "could not rollback transaction"))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
entry := &model.ProjectEntry{}
|
|
|
|
if err := tx.One("ID", projectID, entry); err != nil {
|
|
|
|
if err == storm.ErrNotFound {
|
|
|
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
panic(errors.Wrapf(err, "could not find project '%s'", projectID))
|
|
|
|
}
|
|
|
|
|
|
|
|
if entry.Version != patchReq.Version {
|
|
|
|
if err := writeJSON(w, http.StatusConflict, &projectResponse{entry.Version, entry.Project}); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not write json response"))
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
projectData, err := json.Marshal(entry.Project)
|
|
|
|
if err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not marshal project"))
|
|
|
|
}
|
|
|
|
|
|
|
|
projectData, err = jsonpatch.MergePatch(projectData, patchReq.Patch)
|
|
|
|
if err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not merge project patch"))
|
|
|
|
}
|
|
|
|
|
|
|
|
newProject := &model.Project{}
|
|
|
|
if err := json.Unmarshal(projectData, newProject); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not merge project patch"))
|
|
|
|
}
|
|
|
|
|
|
|
|
entry.Version++
|
|
|
|
entry.Project = newProject
|
|
|
|
|
|
|
|
if err := tx.Save(entry); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not save project"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not commit transaction"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := writeJSON(w, http.StatusOK, &projectResponse{entry.Version, entry.Project}); err != nil {
|
|
|
|
panic(errors.Wrap(err, "could not write json response"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleDeleteProject(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func getProjectID(r *http.Request) model.ProjectID {
|
|
|
|
return model.ProjectID(chi.URLParam(r, "projectID"))
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseJSONBody(r *http.Request, payload interface{}) (err error) {
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err = r.Body.Close(); err != nil {
|
|
|
|
err = errors.Wrap(err, "could not close request body")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if err := decoder.Decode(payload); err != nil {
|
|
|
|
return errors.Wrap(err, "could not decode request body")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) error {
|
|
|
|
encoder := json.NewEncoder(w)
|
|
|
|
encoder.SetIndent("", " ")
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
return encoder.Encode(data)
|
|
|
|
}
|