Add versioned board canvas persistence and stable SEO slugs
Foundry Go checks / check (push) Successful in 7m30s
Foundry Go checks / check (pull_request) Successful in 8m43s

This commit is contained in:
2026-11-06 02:47:27 +08:00
parent e8ed5e4f9f
commit 6f36ef27bb
9 changed files with 548 additions and 26 deletions
+25 -13
View File
@@ -11,7 +11,7 @@ func (s *service) workspaces(w http.ResponseWriter, r *http.Request, project, ac
switch r.Method {
case http.MethodGet:
rows, err := s.db.QueryContext(r.Context(), `
SELECT w.id::text,w.name,m.role,w.owner_id::text FROM workspaces w
SELECT w.id::text,w.name,m.role,w.owner_id::text,w.slug FROM workspaces w
JOIN workspace_members m ON (m.project,m.workspace_id)=(w.project,w.id)
WHERE m.project=$1 AND m.account_id=$2 ORDER BY lower(w.name),w.id LIMIT 200
`, project, account)
@@ -21,11 +21,11 @@ WHERE m.project=$1 AND m.account_id=$2 ORDER BY lower(w.name),w.id LIMIT 200
defer rows.Close()
items := []response{}
for rows.Next() {
var id, name, role, owner string
if err := rows.Scan(&id, &name, &role, &owner); err != nil {
var id, name, role, owner, slug string
if err := rows.Scan(&id, &name, &role, &owner, &slug); err != nil {
return err
}
items = append(items, response{"id": id, "name": name, "role": role, "owner_id": owner})
items = append(items, response{"id": id, "name": name, "role": role, "owner_id": owner, "slug": slug})
}
if err := rows.Err(); err != nil {
return err
@@ -33,13 +33,14 @@ WHERE m.project=$1 AND m.account_id=$2 ORDER BY lower(w.name),w.id LIMIT 200
reply(w, 200, response{"items": items})
case http.MethodPost:
var input struct {
Name string `json:"name"`
Name string `json:"name"`
Slug *string `json:"slug,omitempty"`
}
if !decode(w, r, &input) {
return nil
}
input.Name = strings.TrimSpace(input.Name)
if input.Name == "" || len(input.Name) > 160 {
if input.Name == "" || len(input.Name) > 160 || !validSlug(input.Slug) {
reject(w, 400, "invalid_workspace")
return nil
}
@@ -52,10 +53,20 @@ WHERE m.project=$1 AND m.account_id=$2 ORDER BY lower(w.name),w.id LIMIT 200
if err != nil {
return err
}
var slug string
if input.Slug != nil {
_, err = tx.ExecContext(r.Context(), "UPDATE workspaces SET slug=$3 WHERE project=$1 AND id=$2", project, id, *input.Slug)
if err != nil {
return err
}
}
if err = tx.QueryRowContext(r.Context(), "SELECT slug FROM workspaces WHERE project=$1 AND id=$2", project, id).Scan(&slug); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
reply(w, 201, response{"id": id, "name": input.Name, "role": "admin", "owner_id": account})
reply(w, 201, response{"id": id, "name": input.Name, "role": "admin", "owner_id": account, "slug": slug})
default:
reject(w, 405, "method_not_allowed")
}
@@ -104,11 +115,11 @@ func (s *service) workspace(w http.ResponseWriter, r *http.Request, project, acc
}
switch r.Method {
case http.MethodGet:
var name string
if err := tx.QueryRowContext(r.Context(), "SELECT name FROM workspaces WHERE project=$1 AND id=$2", project, workspace).Scan(&name); err != nil {
var name, slug string
if err := tx.QueryRowContext(r.Context(), "SELECT name,slug FROM workspaces WHERE project=$1 AND id=$2", project, workspace).Scan(&name, &slug); err != nil {
return err
}
reply(w, 200, response{"id": workspace, "name": name, "role": role, "owner_id": owner})
reply(w, 200, response{"id": workspace, "name": name, "role": role, "owner_id": owner, "slug": slug})
return nil
case http.MethodPatch:
if role != "admin" {
@@ -116,17 +127,18 @@ func (s *service) workspace(w http.ResponseWriter, r *http.Request, project, acc
return nil
}
var input struct {
Name string `json:"name"`
Name string `json:"name"`
Slug *string `json:"slug,omitempty"`
}
if !decode(w, r, &input) {
return nil
}
input.Name = strings.TrimSpace(input.Name)
if input.Name == "" || len(input.Name) > 160 {
if input.Name == "" || len(input.Name) > 160 || !validSlug(input.Slug) {
reject(w, 400, "invalid_workspace")
return nil
}
_, err = tx.ExecContext(r.Context(), "UPDATE workspaces SET name=$3,updated_at=now() WHERE project=$1 AND id=$2", project, workspace, input.Name)
_, err = tx.ExecContext(r.Context(), "UPDATE workspaces SET name=$3,slug=COALESCE($4,slug),updated_at=now() WHERE project=$1 AND id=$2", project, workspace, input.Name, input.Slug)
case http.MethodDelete:
if owner != account {
reject(w, 403, "owner_required")