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 }