214 lines
6.6 KiB
Go
214 lines
6.6 KiB
Go
package platform
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
func (s *service) dissolveSubgraph(w http.ResponseWriter, r *http.Request, project, account string) error {
|
|
if r.Method != http.MethodPost {
|
|
reject(w, 405, "method_not_allowed")
|
|
return nil
|
|
}
|
|
if !acceptsCanvasV2(r) {
|
|
reject(w, 409, "canvas_schema_upgrade_required")
|
|
return nil
|
|
}
|
|
key := canvasIdempotencyKey(r)
|
|
frame := r.PathValue("frame")
|
|
if !uuidSlugPattern.MatchString(key) || !canvasID.MatchString(frame) {
|
|
reject(w, 400, "invalid_subgraph_request")
|
|
return nil
|
|
}
|
|
var input struct {
|
|
ExpectedVersions map[string]int64 `json:"expected_versions"`
|
|
}
|
|
if !decode(w, r, &input) {
|
|
return nil
|
|
}
|
|
if len(input.ExpectedVersions) == 0 {
|
|
reject(w, 428, "version_required")
|
|
return nil
|
|
}
|
|
tx, err := s.db.BeginTx(r.Context(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
workspace, board := r.PathValue("workspace"), r.PathValue("material")
|
|
_, role, err := lockMaterial(r.Context(), tx, project, workspace, board, account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if role == "viewer" {
|
|
reject(w, 403, "forbidden")
|
|
return nil
|
|
}
|
|
action := "dissolve_subgraph/" + frame
|
|
hash := payloadDigest(input)
|
|
done, err := transactionRetry(w, r, tx, project, account, board, action, key, hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if done {
|
|
return nil
|
|
}
|
|
var child string
|
|
err = tx.QueryRowContext(r.Context(), `SELECT child_board_id::text FROM canvas_subgraphs WHERE project=$1 AND workspace_id=$2 AND parent_board_id=$3 AND frame_id=$4 AND archived_at IS NULL`, project, workspace, board, frame).Scan(&child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relation, err := readSubgraphRelation(r, tx, project, workspace, child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
archivedRelation := relation
|
|
archivedRelation.Archived = true
|
|
changes := []subgraphRelationChange{{Before: relation, After: archivedRelation}}
|
|
participants := []string{board, child}
|
|
rows, err := tx.QueryContext(r.Context(), `SELECT child_board_id::text FROM canvas_subgraphs WHERE project=$1 AND workspace_id=$2 AND parent_board_id=$3 AND archived_at IS NULL ORDER BY child_board_id`, project, workspace, child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nested := []string{}
|
|
for rows.Next() {
|
|
var id string
|
|
if err = rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
nested = append(nested, id)
|
|
}
|
|
err = rows.Err()
|
|
rows.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, id := range nested {
|
|
before, err := readSubgraphRelation(r, tx, project, workspace, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
after := before
|
|
after.Parent = board
|
|
changes = append(changes, subgraphRelationChange{Before: before, After: after})
|
|
participants = append(participants, id)
|
|
}
|
|
parentBefore, err := loadCanvas(r, tx, project, workspace, board)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
childBefore, err := loadCanvas(r, tx, project, workspace, child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
members := []string{}
|
|
for node := range canvasNodeData(childBefore.Document) {
|
|
members = append(members, node)
|
|
}
|
|
repair, err := planAncestorPortRepair(r, tx, project, workspace, board, child, account, members, parentBefore.Document)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if repair != nil {
|
|
participants = append(participants, repair.Ancestor)
|
|
}
|
|
sort.Strings(participants)
|
|
if len(input.ExpectedVersions) != len(participants) {
|
|
reject(w, 428, "version_required")
|
|
return nil
|
|
}
|
|
snapshots := map[string]canvasSnapshot{}
|
|
beforeSnapshots := []canvasSnapshot{}
|
|
for _, id := range participants {
|
|
_, role, err := lockMaterial(r.Context(), tx, project, workspace, id, account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if role == "viewer" {
|
|
reject(w, 403, "forbidden")
|
|
return nil
|
|
}
|
|
snapshot, err := loadCanvas(r, tx, project, workspace, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, exists := input.ExpectedVersions[id]; !exists {
|
|
reject(w, 428, "version_required")
|
|
return nil
|
|
}
|
|
if input.ExpectedVersions[id] != snapshot.Version {
|
|
reject(w, 412, "version_conflict")
|
|
return nil
|
|
}
|
|
snapshots[id] = snapshot
|
|
beforeSnapshots = append(beforeSnapshots, snapshot)
|
|
}
|
|
parent, childSnapshot := snapshots[board], snapshots[child]
|
|
parentDoc, childDoc, err := dissolveDocuments(parent.Document, childSnapshot.Document, frame)
|
|
if err != nil {
|
|
if err == errGraphOwnership {
|
|
reject(w, 409, "ownership_conflict")
|
|
} else {
|
|
reject(w, 413, "graph_limit_exceeded")
|
|
}
|
|
return nil
|
|
}
|
|
if repair != nil {
|
|
documents := map[string]json.RawMessage{board: parentDoc, repair.Ancestor: snapshots[repair.Ancestor].Document}
|
|
if err = applyAncestorPortRepair(repair, board, board, documents); err != nil {
|
|
return err
|
|
}
|
|
parentDoc = documents[board]
|
|
ancestor := snapshots[repair.Ancestor]
|
|
ancestor.Document = documents[repair.Ancestor]
|
|
snapshots[repair.Ancestor] = ancestor
|
|
}
|
|
parent.Document = parentDoc
|
|
childSnapshot.Document = childDoc
|
|
snapshots[board] = parent
|
|
snapshots[child] = childSnapshot
|
|
// Reparent nested children first. The archived child keeps a recovery record,
|
|
// while live ownership immediately follows the nodes flattened into the parent.
|
|
for i := len(changes) - 1; i >= 0; i-- {
|
|
if err = writeSubgraphRelation(r, tx, project, workspace, changes[i].After); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
updated := []canvasSnapshot{}
|
|
versions := map[string]int64{}
|
|
for _, id := range participants {
|
|
snapshot := snapshots[id]
|
|
snapshot.Version++
|
|
if err = s.saveCanvas(r, tx, project, workspace, snapshot); err != nil {
|
|
return err
|
|
}
|
|
updated = append(updated, snapshot)
|
|
versions[id] = snapshot.Version
|
|
snapshots[id] = snapshot
|
|
}
|
|
var id string
|
|
var receiptExpiry time.Time
|
|
err = tx.QueryRowContext(r.Context(), `SELECT gen_random_uuid()::text,now()+interval '7 days'`).Scan(&id, &receiptExpiry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result := response{"transaction_id": id, "parent": snapshots[board], "snapshots": updated, "archived_board_ids": []string{child}, "undo_receipt": response{"transaction_id": id, "valid_for_seconds": 604800, "expires_at": receiptExpiry}}
|
|
encoded, _ := json.Marshal(result)
|
|
beforeJSON, _ := json.Marshal(beforeSnapshots)
|
|
versionsJSON, _ := json.Marshal(versions)
|
|
relationsJSON, _ := json.Marshal(changes)
|
|
_, err = tx.ExecContext(r.Context(), `INSERT INTO canvas_transactions(project,workspace_id,board_id,id,account_id,action,idempotency_key,payload_hash,response_status,response_body,before_state,after_versions,relation_changes) VALUES($1,$2,$3,$4,$5,$6,$7,$8,200,$9,$10,$11,$12)`, project, workspace, board, id, account, action, key, hash, encoded, beforeJSON, versionsJSON, relationsJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
setVersion(w, versions[board])
|
|
reply(w, 200, result)
|
|
return nil
|
|
}
|