fix(canvas): 画布版本 - 合并 PR #528 防止旧内容覆盖并支持历史恢复

将画布 revision CAS 与历史快照接到 schema v23,保留当前顶栏积分弹窗和已有菜单入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCatandCursor committed 2026-09-18 10:40:23 +08:00
commit aae7b15ccc
69 files changed
+3571 -293

No files matched your search

@@ -49,6 +49,9 @@ func (r *Repository) DeleteAdminResources(resources []model.Resource, deletionJo
resourceIDs = append(resourceIDs, resource.ID)
}
return r.db.Transaction(func(tx *gorm.DB) error {
if err := New(tx).RequireNoCanvasHistoryReferences(resourceIDs); err != nil {
return err
}
var current []model.Resource
query := tx.Where("id IN ?", resourceIDs)
if r.Dialect() == "postgres" {
@@ -0,0 +1,152 @@
package repository
import (
"errors"
"slices"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var ErrCanvasHistoryResourceMissing = errors.New("canvas history resource missing")
var ErrCanvasHistoryResourceReferenced = errors.New("resource referenced by canvas history")
const canvasSnapshotSummaryColumns = "id, canvas_id, user_id, revision, title, node_count, connection_count, payload_bytes, reason, content_updated_at, created_at"
func (r *Repository) CanvasProjectMetadata(userID, id string) (*model.CanvasProject, error) {
var project model.CanvasProject
err := r.db.Omit("payload_json").Where("id = ? AND user_id = ?", id, userID).First(&project).Error
return &project, err
}
func (r *Repository) CanvasSnapshots(userID, canvasID string, limit int) ([]model.CanvasSnapshot, error) {
items := []model.CanvasSnapshot{}
err := r.db.Select(canvasSnapshotSummaryColumns).Where("user_id = ? AND canvas_id = ?", userID, canvasID).
Order("revision DESC").Limit(limit).Find(&items).Error
return items, err
}
func (r *Repository) CanvasSnapshot(userID, canvasID, id string) (*model.CanvasSnapshot, error) {
var item model.CanvasSnapshot
err := r.db.Where("id = ? AND user_id = ? AND canvas_id = ?", id, userID, canvasID).First(&item).Error
return &item, err
}
// The successful CAS takes the canvas row lock before inspecting the history.
// Snapshot creation, reference protection and retention commit with the content.
func (r *Repository) SaveCanvasWithSnapshot(project *model.CanvasProject, snapshot *model.CanvasSnapshot, resourceIDs, restoredResourceIDs []string, cutoff time.Time, limit int, force bool) error {
next := *project
err := r.db.Transaction(func(tx *gorm.DB) error {
if err := New(tx).UpsertCanvasProject(&next); err != nil {
return err
}
if snapshot == nil {
return nil
}
var last model.CanvasSnapshot
err := tx.Select("id", "revision", "created_at").Where("canvas_id = ?", project.ID).Order("revision DESC").First(&last).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
capture := err != nil || (last.Revision != snapshot.Revision && (force || !last.CreatedAt.After(cutoff)))
requiredIDs := append([]string{}, restoredResourceIDs...)
if capture {
requiredIDs = append(requiredIDs, resourceIDs...)
}
slices.Sort(requiredIDs)
requiredIDs = slices.Compact(requiredIDs)
if len(requiredIDs) > 0 {
var resources []model.Resource
query := tx.Select("id").Where("id IN ? AND status = ?", requiredIDs, model.ResourceStatusReady).Order("id")
if r.Dialect() == "postgres" {
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
}
if err := query.Find(&resources).Error; err != nil {
return err
}
if len(resources) != len(requiredIDs) {
return ErrCanvasHistoryResourceMissing
}
}
if !capture {
return nil
}
if err := tx.Create(snapshot).Error; err != nil {
return err
}
refs := make([]model.CanvasSnapshotResource, 0, len(resourceIDs))
for _, id := range resourceIDs {
refs = append(refs, model.CanvasSnapshotResource{SnapshotID: snapshot.ID, ResourceID: id})
}
if len(refs) > 0 {
if err := tx.CreateInBatches(refs, 200).Error; err != nil {
return err
}
}
var expired []string
if err := tx.Model(&model.CanvasSnapshot{}).Where("canvas_id = ?", project.ID).Order("revision DESC").Offset(limit).Limit(100).Pluck("id", &expired).Error; err != nil {
return err
}
return deleteCanvasSnapshots(tx, expired)
})
if err == nil {
*project = next
}
return err
}
func (r *Repository) CanvasHistoryReferencesObject(resource *model.Resource) (bool, error) {
var count int64
aliases := r.db.Model(&model.Resource{}).Select("id").Where("endpoint = ? AND bucket = ? AND object_key = ?", resource.Endpoint, resource.Bucket, resource.ObjectKey)
err := r.db.Model(&model.CanvasSnapshotResource{}).
Where("resource_id = ? OR resource_id IN (?)", resource.ID, aliases).
Count(&count).Error
return count > 0, err
}
func deleteCanvasSnapshots(tx *gorm.DB, ids []string) error {
if len(ids) == 0 {
return nil
}
if err := tx.Where("snapshot_id IN ?", ids).Delete(&model.CanvasSnapshotResource{}).Error; err != nil {
return err
}
return tx.Where("id IN ?", ids).Delete(&model.CanvasSnapshot{}).Error
}
func (r *Repository) CanvasHistoryResourceReferences(resourceIDs []string) ([]ResourceDirectReference, error) {
refs := []ResourceDirectReference{}
if len(resourceIDs) == 0 {
return refs, nil
}
err := r.db.Table("canvas_snapshot_resources AS refs").
Select("'画布历史版本' AS kind, snapshots.canvas_id AS id, snapshots.title, refs.resource_id").
Joins("JOIN canvas_snapshots AS snapshots ON snapshots.id = refs.snapshot_id").
Where("refs.resource_id IN ?", resourceIDs).Distinct().Scan(&refs).Error
return refs, err
}
func (r *Repository) RequireNoCanvasHistoryReferences(resourceIDs []string) error {
if len(resourceIDs) == 0 {
return nil
}
var resources []model.Resource
query := r.db.Select("id").Where("id IN ?", resourceIDs).Order("id")
if r.Dialect() == "postgres" {
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
}
if err := query.Find(&resources).Error; err != nil {
return err
}
refs, err := r.CanvasHistoryResourceReferences(resourceIDs)
if err != nil {
return err
}
if len(refs) > 0 {
return ErrCanvasHistoryResourceReferenced
}
return nil
}
@@ -0,0 +1,151 @@
package repository
import (
"errors"
"fmt"
"sync"
"testing"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
)
func testCanvasHistory(t *testing.T, db *gorm.DB) {
t.Helper()
if db.Dialector.Name() == "sqlite" {
if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
t.Fatal(err)
}
}
if err := db.AutoMigrate(&model.CanvasSnapshot{}, &model.CanvasSnapshotResource{}, &model.CanvasShare{}, &model.Task{}); err != nil {
t.Fatal(err)
}
repo := New(db)
now := time.Now().UTC().Truncate(time.Millisecond)
project := model.CanvasProject{ID: "history-canvas", UserID: "owner", Title: "original", PayloadJSON: `{"nodes":[{"id":"video"}]}`, UpdatedAt: now}
if err := repo.UpsertCanvasProject(&project); err != nil {
t.Fatal(err)
}
resource := model.Resource{ID: "history-video", UserID: "owner", Status: model.ResourceStatusReady}
if err := db.Create(&resource).Error; err != nil {
t.Fatal(err)
}
snapshot := func(p model.CanvasProject, at time.Time) *model.CanvasSnapshot {
return &model.CanvasSnapshot{ID: fmt.Sprintf("history-%d", p.Revision), CanvasID: p.ID, UserID: p.UserID, Revision: p.Revision, Title: p.Title, PayloadJSON: p.PayloadJSON, NodeCount: 1, CreatedAt: at}
}
save := func(at time.Time, force bool) {
t.Helper()
old := project
project.Title = fmt.Sprintf("edit-%d", old.Revision)
if err := repo.SaveCanvasWithSnapshot(&project, snapshot(old, at), []string{resource.ID}, nil, at.Add(-5*time.Minute), 20, force); err != nil {
t.Fatal(err)
}
}
save(now, false)
save(now.Add(4*time.Minute), false)
metadata, err := repo.CanvasProjectMetadata(project.UserID, project.ID)
if err != nil || metadata.Revision != project.Revision || metadata.UserID != project.UserID || metadata.PayloadJSON != "" {
t.Fatalf("canvas metadata: %#v %v", metadata, err)
}
items, err := repo.CanvasSnapshots("owner", project.ID, 20)
if err != nil || len(items) != 1 || items[0].PayloadJSON != "" {
t.Fatalf("summary/throttle: %#v %v", items, err)
}
save(now.Add(5*time.Minute), false)
items, _ = repo.CanvasSnapshots("owner", project.ID, 20)
if len(items) != 2 {
t.Fatalf("5 minute boundary = %d", len(items))
}
if err := db.Transaction(func(tx *gorm.DB) error {
return New(tx).RequireNoCanvasHistoryReferences([]string{resource.ID})
}); !errors.Is(err, ErrCanvasHistoryResourceReferenced) {
t.Fatalf("history protection = %v", err)
}
refs, err := repo.CanvasHistoryResourceReferences([]string{resource.ID})
if err != nil || len(refs) == 0 || refs[0].Kind != "画布历史版本" {
t.Fatalf("references = %#v %v", refs, err)
}
if err := db.Delete(&resource).Error; err == nil {
t.Fatal("database allowed deletion of a history resource")
}
if _, err := repo.CanvasSnapshot("other", project.ID, items[0].ID); !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("wrong owner = %v", err)
}
// Snapshot failure must roll back the successful content CAS as well.
before := project
failed := project
failed.Title = "must rollback"
err = repo.SaveCanvasWithSnapshot(&failed, snapshot(project, now.Add(10*time.Minute)), []string{"missing"}, nil, now, 20, true)
if !errors.Is(err, ErrCanvasHistoryResourceMissing) {
t.Fatalf("missing resource = %v", err)
}
stored, _ := repo.CanvasProjectForUser("owner", project.ID)
if stored.Revision != before.Revision || stored.Title != before.Title || failed.Revision != before.Revision {
t.Fatal("failed snapshot changed content/revision")
}
// Concurrent writers create exactly one history entry along with one new revision.
start := make(chan struct{})
results := make(chan error, 2)
var workers sync.WaitGroup
for i := 0; i < 2; i++ {
workers.Add(1)
go func() {
defer workers.Done()
copy := project
<-start
results <- repo.SaveCanvasWithSnapshot(&copy, snapshot(project, now.Add(10*time.Minute)), []string{resource.ID}, nil, now, 20, true)
}()
}
close(start)
workers.Wait()
close(results)
success, conflict := 0, 0
for err := range results {
if err == nil {
success++
} else if errors.Is(err, ErrCanvasRevisionConflict) {
conflict++
} else {
t.Fatal(err)
}
}
if success != 1 || conflict != 1 {
t.Fatalf("concurrent saves: success=%d conflict=%d", success, conflict)
}
stored, _ = repo.CanvasProjectForUser("owner", project.ID)
project = *stored
for i := 0; i < 23; i++ {
save(now.Add(time.Duration(20+i*5)*time.Minute), false)
}
var count, refCount int64
db.Model(&model.CanvasSnapshot{}).Where("canvas_id = ?", project.ID).Count(&count)
db.Model(&model.CanvasSnapshotResource{}).Count(&refCount)
if count != 20 || refCount != 20 {
t.Fatalf("retention: snapshots=%d refs=%d", count, refCount)
}
save(now.Add(131*time.Minute), true)
items, _ = repo.CanvasSnapshots("owner", project.ID, 20)
if items[0].Revision != project.Revision-1 || len(items) != 20 {
t.Fatal("forced pre-restore backup missing or retention exceeded")
}
if err := repo.DeleteCanvasProject("owner", project.ID); err != nil {
t.Fatal(err)
}
db.Model(&model.CanvasSnapshot{}).Where("canvas_id = ?", project.ID).Count(&count)
db.Model(&model.CanvasSnapshotResource{}).Count(&refCount)
if count != 0 || refCount != 0 {
t.Fatal("canvas deletion left orphan history")
}
if err := db.Transaction(func(tx *gorm.DB) error {
return New(tx).RequireNoCanvasHistoryReferences([]string{resource.ID})
}); err != nil {
t.Fatalf("expired reference not released: %v", err)
}
if err := db.Delete(&resource).Error; err != nil {
t.Fatalf("expired foreign key not released: %v", err)
}
}
@@ -0,0 +1,159 @@
package repository
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"sync"
"testing"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func TestCanvasRevision(t *testing.T) {
t.Run("sqlite", func(t *testing.T) {
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "canvas.db")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatal(err)
}
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(1)
t.Cleanup(func() { _ = sqlDB.Close() })
testCanvasRevision(t, db)
})
t.Run("postgres", func(t *testing.T) {
dsn := os.Getenv("CANVAS_TEST_POSTGRES_DSN")
if dsn == "" {
t.Skip("set CANVAS_TEST_POSTGRES_DSN to an isolated test database")
}
config := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
admin, err := gorm.Open(postgres.Open(dsn), config)
if err != nil {
t.Fatal(err)
}
schema := fmt.Sprintf("canvas_revision_%d", time.Now().UnixNano())
if err := admin.Exec("CREATE SCHEMA " + schema).Error; err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = admin.Exec("DROP SCHEMA " + schema + " CASCADE").Error
connection, _ := admin.DB()
_ = connection.Close()
})
parsed, err := url.Parse(dsn)
if err != nil {
t.Fatal(err)
}
query := parsed.Query()
query.Set("search_path", schema)
parsed.RawQuery = query.Encode()
db, err := gorm.Open(postgres.Open(parsed.String()), config)
if err != nil {
t.Fatal(err)
}
connection, _ := db.DB()
t.Cleanup(func() { _ = connection.Close() })
testCanvasRevision(t, db)
})
}
func testCanvasRevision(t *testing.T, db *gorm.DB) {
t.Helper()
if err := db.AutoMigrate(&model.CanvasProject{}, &model.CanvasUnitLink{}, &model.Project{}); err != nil {
t.Fatal(err)
}
repo := New(db)
initial := model.CanvasProject{ID: "canvas", UserID: "owner", Title: "initial", PayloadJSON: `{"nodes":[{"id":"old"}]}`}
if err := repo.UpsertCanvasProject(&initial); err != nil {
t.Fatal(err)
}
if initial.Revision != 1 {
t.Fatalf("initial revision = %d", initial.Revision)
}
start := make(chan struct{})
results := make(chan error, 2)
var workers sync.WaitGroup
for _, title := range []string{"editor A", "editor B"} {
workers.Add(1)
go func(title string) {
defer workers.Done()
copy := initial
copy.Title = title
copy.PayloadJSON = fmt.Sprintf(`{"nodes":[{"id":%q}]}`, title)
<-start
results <- repo.UpsertCanvasProject(&copy)
}(title)
}
close(start)
workers.Wait()
close(results)
successes, conflicts := 0, 0
for err := range results {
if err == nil {
successes++
} else if errors.Is(err, ErrCanvasRevisionConflict) {
conflicts++
} else {
t.Fatal(err)
}
}
if successes != 1 || conflicts != 1 {
t.Fatalf("successes=%d conflicts=%d", successes, conflicts)
}
winner, err := repo.CanvasProjectForUser("owner", initial.ID)
if err != nil {
t.Fatal(err)
}
if winner.Revision != 2 {
t.Fatalf("winner revision = %d", winner.Revision)
}
old := initial
old.UpdatedAt = time.Now().Add(time.Hour)
if err := repo.UpsertCanvasProject(&old); !errors.Is(err, ErrCanvasRevisionConflict) {
t.Fatalf("stale write = %v", err)
}
wrongOwner := *winner
wrongOwner.UserID = "another-user"
if err := repo.UpsertCanvasProject(&wrongOwner); !errors.Is(err, ErrCanvasRevisionConflict) {
t.Fatalf("wrong owner = %v", err)
}
createCollision := initial
createCollision.Revision = 0
if err := repo.UpsertCanvasProject(&createCollision); !errors.Is(err, ErrCanvasRevisionConflict) {
t.Fatalf("create collision = %v", err)
}
stored, _ := repo.CanvasProjectForUser("owner", initial.ID)
if stored.PayloadJSON != winner.PayloadJSON || stored.Revision != 2 {
t.Fatal("rejected save changed the canvas")
}
if err := repo.AssignCanvasToProject("owner", initial.ID, "project"); err != nil {
t.Fatal(err)
}
if err := repo.UpsertCanvasProject(winner); !errors.Is(err, ErrCanvasRevisionConflict) {
t.Fatalf("association did not invalidate old content: %v", err)
}
if err := repo.UnassignCanvasFromProject("owner", "project", initial.ID, `{"nodes":[]}`, time.Now(), 2); err == nil {
t.Fatal("stale unassignment replaced canvas content")
}
stored, _ = repo.CanvasProjectForUser("owner", initial.ID)
if stored.PayloadJSON != winner.PayloadJSON || stored.Revision != 3 {
t.Fatal("stale unassignment changed the canvas")
}
if err := db.Delete(&model.CanvasProject{}, "id = ?", initial.ID).Error; err != nil {
t.Fatal(err)
}
if err := repo.UpsertCanvasProject(stored); !errors.Is(err, ErrCanvasRevisionConflict) {
t.Fatalf("deleted canvas was recreated: %v", err)
}
t.Run("history", func(t *testing.T) { testCanvasHistory(t, db) })
}
+1 -11
View File
@@ -105,17 +105,7 @@ func (r *Repository) RevokeCreationSubmissions(runID string) error {
return r.db.Model(&model.CreationSubmission{}).Where("run_id = ? AND task_id IS NULL AND revoked_at IS NULL", runID).Update("revoked_at", time.Now()).Error
}
func (r *Repository) CreateCreationCanvas(canvas *model.CanvasProject) error {
return r.db.Create(canvas).Error
}
func (r *Repository) CompareSaveCreationCanvas(canvas *model.CanvasProject, previous string) error {
result := r.db.Model(&model.CanvasProject{}).Where("id = ? AND user_id = ? AND payload_json = ?", canvas.ID, canvas.UserID, previous).Updates(map[string]any{"payload_json": canvas.PayloadJSON, "title": canvas.Title, "updated_at": time.Now()})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return ErrCreationConflict
}
return nil
return r.UpsertCanvasProject(canvas)
}
// Capture server-side configuration versions, never channel credentials.
+49 -25
View File
@@ -35,6 +35,8 @@ var ErrProjectHasActiveTasks = errors.New("project has active tasks")
var ErrProjectUnitShotsChanged = errors.New("project unit shots changed")
var ErrCanvasRevisionConflict = errors.New("canvas revision changed")
type Repository struct {
db *gorm.DB
}
@@ -1152,7 +1154,7 @@ func (r *Repository) CanvasProjects(userID string) ([]model.CanvasProject, error
func (r *Repository) CanvasProjectSummaries(userID string) ([]model.CanvasProject, error) {
var projects []model.CanvasProject
err := r.db.Select("id", "title", "created_at", "updated_at").Order("updated_at desc").Find(&projects, "user_id = ?", userID).Error
err := r.db.Select("id", "title", "revision", "created_at", "updated_at").Order("updated_at desc").Find(&projects, "user_id = ?", userID).Error
return projects, err
}
@@ -1165,17 +1167,51 @@ func (r *Repository) CanvasProjectForUser(userID string, id string) (*model.Canv
}
func (r *Repository) UpsertCanvasProject(project *model.CanvasProject) error {
expected := project.Revision
if expected < 0 {
return ErrCanvasRevisionConflict
}
if expected == 0 {
created := *project
created.Revision = 1
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&created)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return ErrCanvasRevisionConflict
}
project.Revision = 1
return nil
}
// The revision predicate and increment must be in the same SQL statement.
// A missing row is a conflict, never an invitation to recreate a deleted canvas.
result := r.db.Model(&model.CanvasProject{}).
Where("id = ? AND user_id = ?", project.ID, project.UserID).
Updates(map[string]any{"project_id": project.ProjectID, "title": project.Title, "payload_json": project.PayloadJSON, "updated_at": project.UpdatedAt})
if result.Error != nil || result.RowsAffected > 0 {
Where("id = ? AND user_id = ? AND revision = ?", project.ID, project.UserID, expected).
Updates(map[string]any{"project_id": project.ProjectID, "title": project.Title, "payload_json": project.PayloadJSON, "updated_at": project.UpdatedAt, "revision": expected + 1})
if result.Error != nil {
return result.Error
}
return r.db.Create(project).Error
if result.RowsAffected != 1 {
return ErrCanvasRevisionConflict
}
project.Revision = expected + 1
return nil
}
func (r *Repository) DeleteCanvasProject(userID string, id string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Serialize deletion with saves before reading the history IDs to remove.
if err := tx.Model(&model.CanvasProject{}).Where("user_id = ? AND id = ?", userID, id).UpdateColumn("revision", gorm.Expr("revision")).Error; err != nil {
return err
}
var snapshotIDs []string
if err := tx.Model(&model.CanvasSnapshot{}).Where("user_id = ? AND canvas_id = ?", userID, id).Pluck("id", &snapshotIDs).Error; err != nil {
return err
}
if err := deleteCanvasSnapshots(tx, snapshotIDs); err != nil {
return err
}
if err := tx.Where("user_id = ? AND project_id = ?", userID, id).Delete(&model.CanvasShare{}).Error; err != nil {
return err
}
@@ -1257,8 +1293,8 @@ func (r *Repository) DeleteProject(userID string, id string, canvasUpdates []mod
}
for _, canvas := range canvasUpdates {
result := tx.Model(&model.CanvasProject{}).
Where("id = ? AND user_id = ? AND project_id = ?", canvas.ID, userID, id).
Updates(map[string]any{"project_id": "", "payload_json": canvas.PayloadJSON, "updated_at": canvas.UpdatedAt})
Where("id = ? AND user_id = ? AND project_id = ? AND revision = ?", canvas.ID, userID, id, canvas.Revision).
Updates(map[string]any{"project_id": "", "payload_json": canvas.PayloadJSON, "updated_at": canvas.UpdatedAt, "revision": canvas.Revision + 1})
if result.Error != nil {
return result.Error
}
@@ -1466,13 +1502,13 @@ func (r *Repository) UpsertCanvasUnitLink(link *model.CanvasUnitLink) error {
func (r *Repository) ProjectCanvasSummaries(userID string, projectID string) ([]model.CanvasProject, error) {
var canvases []model.CanvasProject
err := r.db.Select("id", "user_id", "project_id", "title", "created_at", "updated_at").Where("user_id = ? AND project_id = ?", userID, projectID).Order("updated_at desc").Find(&canvases).Error
err := r.db.Select("id", "user_id", "project_id", "title", "revision", "created_at", "updated_at").Where("user_id = ? AND project_id = ?", userID, projectID).Order("updated_at desc").Find(&canvases).Error
return canvases, err
}
func (r *Repository) ProjectCanvasDocuments(userID string, projectID string) ([]model.CanvasProject, error) {
var canvases []model.CanvasProject
err := r.db.Select("id", "title", "payload_json").Where("user_id = ? AND project_id = ?", userID, projectID).Find(&canvases).Error
err := r.db.Select("id", "title", "payload_json", "revision").Where("user_id = ? AND project_id = ?", userID, projectID).Find(&canvases).Error
return canvases, err
}
@@ -1496,16 +1532,16 @@ func (r *Repository) DeleteCanvasUnitLink(projectID string, canvasID string, uni
}
func (r *Repository) AssignCanvasToProject(userID string, canvasID string, projectID string) error {
return r.db.Model(&model.CanvasProject{}).Where("id = ? AND user_id = ?", canvasID, userID).Update("project_id", projectID).Error
return r.db.Model(&model.CanvasProject{}).Where("id = ? AND user_id = ?", canvasID, userID).Updates(map[string]any{"project_id": projectID, "revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
}
func (r *Repository) UnassignCanvasFromProject(userID string, projectID string, canvasID string, payloadJSON string, updatedAt time.Time) error {
func (r *Repository) UnassignCanvasFromProject(userID string, projectID string, canvasID string, payloadJSON string, updatedAt time.Time, revision int64) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("project_id = ? AND canvas_id = ?", projectID, canvasID).Delete(&model.CanvasUnitLink{}).Error; err != nil {
return err
}
result := tx.Model(&model.CanvasProject{}).Where("id = ? AND user_id = ? AND project_id = ?", canvasID, userID, projectID).Updates(map[string]any{
"project_id": "", "payload_json": payloadJSON, "updated_at": updatedAt,
result := tx.Model(&model.CanvasProject{}).Where("id = ? AND user_id = ? AND project_id = ? AND revision = ?", canvasID, userID, projectID, revision).Updates(map[string]any{
"project_id": "", "payload_json": payloadJSON, "updated_at": updatedAt, "revision": revision + 1,
})
if result.Error != nil {
return result.Error
@@ -2351,18 +2387,6 @@ func (r *Repository) DeleteCanvasShare(userID string, projectID string) error {
return r.db.Delete(&model.CanvasShare{}, "user_id = ? AND project_id = ?", userID, projectID).Error
}
func (r *Repository) ReplaceCanvasProjects(userID string, projects []model.CanvasProject) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Delete(&model.CanvasProject{}, "user_id = ?", userID).Error; err != nil {
return err
}
if len(projects) == 0 {
return nil
}
return tx.Create(&projects).Error
})
}
func (r *Repository) PromptTemplates() ([]model.PromptTemplate, error) {
var templates []model.PromptTemplate
err := r.db.Order("operation asc, version desc").Find(&templates).Error
@@ -86,6 +86,11 @@ func (r *Repository) ResourceReferenceSnapshot(userID string, excludingAssetID s
if len(resourceIDs) == 0 {
return snapshot, nil
}
history, err := r.CanvasHistoryResourceReferences(resourceIDs)
if err != nil {
return snapshot, err
}
snapshot.Direct = append(snapshot.Direct, history...)
var assets []model.Asset
assetQuery := r.db.Where("user_id = ? AND id <> ?", userID, excludingAssetID)
@@ -322,6 +327,9 @@ func (r *Repository) AssetBusinessReferences(userID string, assetID string) ([]R
func (r *Repository) DeleteAssetAndResources(userID string, assetID string, resourceIDs []string, deletionJobs []model.ResourceDeletionJob) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := New(tx).RequireNoCanvasHistoryReferences(resourceIDs); err != nil {
return err
}
versionIDs := tx.Model(&model.AssetVersion{}).Select("id").Where("asset_id = ?", assetID)
if err := tx.Where("asset_version_id IN (?)", versionIDs).Delete(&model.ShotAssetReference{}).Error; err != nil {
return err