Files
codex-bot d6ee8942f1
Foundry Go checks / check (push) Successful in 2m40s
Foundry Go checks / check (pull_request) Successful in 2m36s
Implement Canvas V2 subgraphs, assets and asynchronous AI execution
2026-11-06 18:36:00 +08:00

67 lines
2.8 KiB
Go

package platform
import (
"database/sql"
"errors"
"net/http"
)
type subgraphRelation struct {
Parent string `json:"parent_board_id"`
Frame string `json:"frame_id"`
Child string `json:"child_board_id"`
Archived bool `json:"archived"`
}
type subgraphRelationChange struct {
Before subgraphRelation `json:"before"`
After subgraphRelation `json:"after"`
}
func readSubgraphRelation(r *http.Request, tx *sql.Tx, project, workspace, child string) (subgraphRelation, error) {
result := subgraphRelation{Child: child}
err := tx.QueryRowContext(r.Context(), `SELECT parent_board_id::text,frame_id,archived_at IS NOT NULL FROM canvas_subgraphs WHERE project=$1 AND workspace_id=$2 AND child_board_id=$3 FOR UPDATE`, project, workspace, child).Scan(&result.Parent, &result.Frame, &result.Archived)
return result, err
}
func writeSubgraphRelation(r *http.Request, tx *sql.Tx, project, workspace string, relation subgraphRelation) error {
before, err := readSubgraphRelation(r, tx, project, workspace, relation.Child)
if err != nil {
return err
}
if before != relation {
if !relation.Archived {
if err = validateSubgraphPlacement(r, tx, project, workspace, relation.Parent, relation.Child); err != nil {
return err
}
}
if err = invalidateCanvasAISubtree(r, tx, project, workspace, relation.Child); err != nil {
return err
}
}
_, err = tx.ExecContext(r.Context(), `UPDATE canvas_subgraphs SET parent_board_id=$4,frame_id=$5,archived_at=CASE WHEN $6 THEN COALESCE(archived_at,now()) ELSE NULL END WHERE project=$1 AND workspace_id=$2 AND child_board_id=$3`, project, workspace, relation.Child, relation.Parent, relation.Frame, relation.Archived)
return err
}
var errSubgraphCycle = errors.New("subgraph_cycle")
var errSubgraphDepth = errors.New("subgraph_depth_exceeded")
func validateSubgraphPlacement(r *http.Request, tx *sql.Tx, project, workspace, parent, child string) error {
var cycle bool
var depth int
err := tx.QueryRowContext(r.Context(), `WITH RECURSIVE ancestors(id,depth) AS(
SELECT $3::uuid,0 UNION ALL SELECT s.parent_board_id,a.depth+1 FROM ancestors a JOIN canvas_subgraphs s ON s.project=$1 AND s.workspace_id=$2 AND s.child_board_id=a.id AND s.archived_at IS NULL WHERE a.depth<8
), descendants(id,depth) AS(
SELECT $4::uuid,0 UNION ALL SELECT s.child_board_id,d.depth+1 FROM descendants d JOIN canvas_subgraphs s ON s.project=$1 AND s.workspace_id=$2 AND s.parent_board_id=d.id AND s.archived_at IS NULL WHERE d.depth<8
) SELECT EXISTS(SELECT 1 FROM ancestors WHERE id=$4::uuid), (SELECT max(depth) FROM ancestors)+1+(SELECT max(depth) FROM descendants)`, project, workspace, parent, child).Scan(&cycle, &depth)
if err != nil {
return err
}
if cycle {
return errSubgraphCycle
}
if depth > 8 {
return errSubgraphDepth
}
return nil
}