159 lines
5.2 KiB
Go
159 lines
5.2 KiB
Go
package platform
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"math"
|
|
"net/http"
|
|
)
|
|
|
|
type placement struct {
|
|
MaterialID string `json:"material_id"`
|
|
X float64 `json:"x"`
|
|
Y float64 `json:"y"`
|
|
Width float64 `json:"width"`
|
|
Height float64 `json:"height"`
|
|
ZIndex int `json:"z_index"`
|
|
}
|
|
|
|
func (p placement) valid() bool {
|
|
return !math.IsNaN(p.X) && !math.IsNaN(p.Y) && !math.IsNaN(p.Width) && !math.IsNaN(p.Height) &&
|
|
p.X >= -1000000 && p.X <= 1000000 && p.Y >= -1000000 && p.Y <= 1000000 &&
|
|
p.Width >= 1 && p.Width <= 100000 && p.Height >= 1 && p.Height <= 100000 &&
|
|
p.ZIndex >= -1000000 && p.ZIndex <= 1000000
|
|
}
|
|
func (s *service) layouts(w http.ResponseWriter, r *http.Request, project, account string) error {
|
|
if r.Method != http.MethodGet && r.Method != http.MethodPost && r.Method != http.MethodPatch && r.Method != http.MethodDelete {
|
|
reject(w, 405, "method_not_allowed")
|
|
return nil
|
|
}
|
|
workspace, board, layout := r.PathValue("workspace"), r.PathValue("material"), r.PathValue("layout")
|
|
if (layout == "" && (r.Method == http.MethodPatch || r.Method == http.MethodDelete)) || (layout != "" && (r.Method == http.MethodGet || r.Method == http.MethodPost)) {
|
|
reject(w, 405, "method_not_allowed")
|
|
return nil
|
|
}
|
|
tx, err := s.db.BeginTx(r.Context(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
kind, role, err := lockMaterial(r.Context(), tx, project, workspace, board, account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if kind != "board" {
|
|
reject(w, 400, "not_a_board")
|
|
return nil
|
|
}
|
|
var version int64
|
|
if err = tx.QueryRowContext(r.Context(), "SELECT version FROM materials WHERE project=$1 AND workspace_id=$2 AND id=$3", project, workspace, board).Scan(&version); err != nil {
|
|
return err
|
|
}
|
|
if r.Method == http.MethodGet {
|
|
rows, err := tx.QueryContext(r.Context(), `
|
|
SELECT jsonb_build_object('id',l.id,'material_id',l.material_id,'x',l.x,'y',l.y,'width',l.width,'height',l.height,'z_index',l.z_index,'version',l.version)
|
|
FROM board_layouts l WHERE l.project=$1 AND l.workspace_id=$2 AND l.board_id=$3
|
|
AND (EXISTS(SELECT 1 FROM workspace_members m WHERE m.project=l.project AND m.workspace_id=l.workspace_id AND m.account_id=$4)
|
|
OR EXISTS(SELECT 1 FROM material_grants g WHERE (g.project,g.workspace_id,g.material_id)=(l.project,l.workspace_id,l.material_id) AND g.account_id=$4))
|
|
ORDER BY l.z_index,l.id LIMIT 2000
|
|
`, project, workspace, board, account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
items := []json.RawMessage{}
|
|
for rows.Next() {
|
|
var item json.RawMessage
|
|
if err := rows.Scan(&item); err != nil {
|
|
return err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
setVersion(w, version)
|
|
reply(w, 200, response{"items": items, "board_version": version})
|
|
return nil
|
|
}
|
|
if role == "viewer" {
|
|
reject(w, 403, "forbidden")
|
|
return nil
|
|
}
|
|
expected, ok := expectedVersion(w, r)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if expected != version {
|
|
reject(w, 412, "version_conflict")
|
|
return nil
|
|
}
|
|
var input placement
|
|
if r.Method != http.MethodDelete {
|
|
if !decode(w, r, &input) {
|
|
return nil
|
|
}
|
|
if !input.valid() || input.MaterialID == board {
|
|
reject(w, 400, "invalid_layout")
|
|
return nil
|
|
}
|
|
refKind, _, err := lockMaterial(r.Context(), tx, project, workspace, input.MaterialID, account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if refKind == "board" {
|
|
reject(w, 400, "nested_board_not_supported")
|
|
return nil
|
|
}
|
|
}
|
|
var result sql.Result
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
var count int
|
|
if err = tx.QueryRowContext(r.Context(), "SELECT count(*) FROM board_layouts WHERE project=$1 AND workspace_id=$2 AND board_id=$3", project, workspace, board).Scan(&count); err != nil {
|
|
return err
|
|
}
|
|
if count >= 2000 {
|
|
reject(w, 409, "board_full")
|
|
return nil
|
|
}
|
|
err = tx.QueryRowContext(r.Context(), `
|
|
INSERT INTO board_layouts(project,workspace_id,board_id,material_id,x,y,width,height,z_index)
|
|
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id::text
|
|
`, project, workspace, board, input.MaterialID, input.X, input.Y, input.Width, input.Height, input.ZIndex).Scan(&layout)
|
|
case http.MethodPatch:
|
|
result, err = tx.ExecContext(r.Context(), `
|
|
UPDATE board_layouts SET material_id=$5,x=$6,y=$7,width=$8,height=$9,z_index=$10,version=version+1
|
|
WHERE project=$1 AND workspace_id=$2 AND board_id=$3 AND id=$4
|
|
`, project, workspace, board, layout, input.MaterialID, input.X, input.Y, input.Width, input.Height, input.ZIndex)
|
|
case http.MethodDelete:
|
|
result, err = tx.ExecContext(r.Context(), "DELETE FROM board_layouts WHERE project=$1 AND workspace_id=$2 AND board_id=$3 AND id=$4", project, workspace, board, layout)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result != nil {
|
|
n, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
}
|
|
_, err = tx.ExecContext(r.Context(), "UPDATE materials SET version=version+1,modified_at=now() WHERE project=$1 AND workspace_id=$2 AND id=$3", project, workspace, board)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
setVersion(w, version+1)
|
|
if r.Method == http.MethodPost {
|
|
reply(w, 201, response{"id": layout, "board_version": version + 1})
|
|
} else {
|
|
reply(w, 204, nil)
|
|
}
|
|
return nil
|
|
}
|