Files
Agent-System-Backend/internal/store/project_projection.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:47:16 +00:00

150 lines
4.5 KiB
Go

package store
import (
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
"database/sql"
"errors"
"strings"
"time"
)
// lockProjectNames serializes projection identity and name admission across all
// owners. SQLite acquires its write reservation before any reads; PostgreSQL
// uses a transaction-scoped advisory lock shared by every projection mutation.
func (s *Store) lockProjectNames(tx *sql.Tx) error {
if s.dialect.name() == "sqlite" {
_, err := tx.Exec(`UPDATE projects SET id=id WHERE 1=0`)
return err
}
return s.dialect.lockScope(tx, "", "host-project-names")
}
// EnsureProject creates a stable projection or verifies an identical retry.
func (s *Store) EnsureProject(owner, id, name string) (core.Project, error) {
name = strings.TrimSpace(name)
if owner == "" || id == "" || name == "" {
return core.Project{}, errors.New("store: project owner, id and name required")
}
tx, err := s.db.Begin()
if err != nil {
return core.Project{}, err
}
defer tx.Rollback()
if err = s.lockProjectNames(tx); err != nil {
return core.Project{}, err
}
p, err := s.projectMetadata(tx, id)
if err == nil {
if p.Owner != owner {
return core.Project{}, ErrNotFound
}
if p.Name != name {
return core.Project{}, ErrConflict
}
return p, tx.Commit()
}
if !errors.Is(err, ErrNotFound) {
return core.Project{}, err
}
available, err := s.projectNameAvailable(tx, name, id)
if err != nil {
return core.Project{}, err
}
if !available {
return core.Project{}, ErrConflict
}
now := time.Now().UTC()
p = core.Project{ID: id, Owner: owner, Name: name, CreatedAt: now, UpdatedAt: now, Settings: map[string]any{}}
_, err = tx.Exec(s.q(`INSERT INTO projects(id,owner,name,settings,created_at,updated_at) VALUES(?,?,?,?,?,?)`), id, owner, name, "{}", now.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano))
if err != nil {
return core.Project{}, err
}
return p, tx.Commit()
}
func (s *Store) RenameOwnedProject(owner, id, name string) (core.Project, error) {
name = strings.TrimSpace(name)
if name == "" {
return core.Project{}, errors.New("store: project name required")
}
tx, err := s.db.Begin()
if err != nil {
return core.Project{}, err
}
defer tx.Rollback()
if err = s.lockProjectNames(tx); err != nil {
return core.Project{}, err
}
p, err := s.projectMetadata(tx, id)
if err != nil {
return core.Project{}, err
}
if p.Owner != owner {
return core.Project{}, ErrNotFound
}
available, err := s.projectNameAvailable(tx, name, id)
if err != nil {
return core.Project{}, err
}
if !available {
return core.Project{}, ErrConflict
}
p.Name, p.UpdatedAt = name, time.Now().UTC()
_, err = tx.Exec(s.q(`UPDATE projects SET name=?,updated_at=? WHERE id=? AND owner=?`), p.Name, p.UpdatedAt.Format(time.RFC3339Nano), id, owner)
if err != nil {
return core.Project{}, err
}
return p, tx.Commit()
}
type projectQuerier interface {
QueryRow(string, ...any) *sql.Row
Query(string, ...any) (*sql.Rows, error)
}
func (s *Store) projectNameAvailable(q projectQuerier, name, exceptID string) (bool, error) {
// Use the same Go whitespace rule as admission, including legacy raw names.
rows, err := q.Query(s.q(`SELECT name FROM projects WHERE id<>?`), exceptID)
if err != nil {
return false, err
}
defer rows.Close()
name = strings.TrimSpace(name)
for rows.Next() {
var candidate string
if err := rows.Scan(&candidate); err != nil {
return false, err
}
if strings.TrimSpace(candidate) == name {
return false, nil
}
}
return true, rows.Err()
}
func (s *Store) ProjectNameAvailable(name, exceptID string) (bool, error) {
return s.projectNameAvailable(s.db, name, exceptID)
}
func (s *Store) projectMetadata(q projectQuerier, id string) (core.Project, error) {
var p core.Project
var settings, ca, ua string
err := q.QueryRow(s.q(`SELECT id,owner,name,settings,created_at,updated_at FROM projects WHERE id=?`), id).Scan(&p.ID, &p.Owner, &p.Name, &settings, &ca, &ua)
if errors.Is(err, sql.ErrNoRows) {
return p, ErrNotFound
}
if err != nil {
return p, err
}
p.CreatedAt, _ = time.Parse(time.RFC3339Nano, ca)
p.UpdatedAt, _ = time.Parse(time.RFC3339Nano, ua)
p.Settings = map[string]any{}
if settings != "" && settings != "{}" {
p.Settings, _ = unmarshalJSON[map[string]any](settings)
}
return p, nil
}
// ProjectMetadata is an internal trusted lookup. Capabilities must authorize it
// before exposing metadata to another owner.
func (s *Store) ProjectMetadata(id string) (core.Project, error) { return s.projectMetadata(s.db, id) }