refactor(documents): rename document owner fields

This commit is contained in:
2026-07-24 12:29:16 +08:00
parent ab8ba3c03f
commit f2d1fef2d6
16 changed files with 68 additions and 21 deletions

View File

@@ -366,7 +366,7 @@ func (s *Service) DepositItem(userID uint, itemIdentity, projectIdentity string)
var document models.SaDocumentTree
err = s.db.Transaction(func(tx *gorm.DB) error {
document = models.SaDocumentTree{
ProjectID: project.ID, CreatedBy: userID, Kind: models.DocumentKindFile,
ProjectID: project.ID, OwnerID: userID, Kind: models.DocumentKindFile,
Name: name, NormalizedName: strings.ToLower(name), Extension: ".md",
MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(body))),
Revision: 1, ScanStatus: "not_scanned",

View File

@@ -153,7 +153,7 @@ func (s *Service) createNode(userID uint, projectIdentity string, input CreateIn
ProjectID: project.ID, ProjectIdentity: project.Identity,
ParentID: parentID, ParentIdentity: parentIdentity,
ParentScope: identityValue(parentIdentity),
CreatedBy: userID, SourceInboxItemID: input.SourceInboxItemID,
OwnerID: userID, SourceInboxItemID: input.SourceInboxItemID,
Kind: kind, Name: name, NormalizedName: normalizeName(name),
Extension: extension, MimeType: mimeType, Size: size, Revision: 1,
ScanStatus: "not_scanned",

View File

@@ -280,7 +280,7 @@ func createConfirmedObject(tx *gorm.DB, item models.SaInboxItem, suggestion mode
name += ".md"
}
document := models.SaDocumentTree{
ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, CreatedBy: item.CreatedBy,
ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, OwnerID: item.CreatedBy,
SourceInboxItemID: &sourceInboxItemID, Kind: models.DocumentKindFile, Name: name,
NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown; charset=utf-8",
Size: int64(len([]byte(suggestion.Body))), Revision: 1, ScanStatus: "not_scanned",

View File

@@ -162,7 +162,8 @@ func TestConfirmCreatesOnlySelectedSavedSuggestionsWithInboxIdentity(t *testing.
require.Equal(t, "背景资料.md", documents[1].Name)
for _, document := range documents {
require.Equal(t, fixture.project.ID, document.ProjectID)
require.Equal(t, fixture.owner.ID, document.CreatedBy)
require.Equal(t, fixture.owner.ID, document.OwnerID)
require.Equal(t, fixture.owner.Identity, document.OwnerIdentity)
requireSourceInboxIdentity(t, item, document.SourceInboxItemID, document.SourceInboxItemIdentity)
}
var secondContent models.SaDocumentContent
@@ -267,7 +268,7 @@ func TestRepeatedConfirmReturnsPersistedResultAfterRelatedObjectsChange(t *testi
sourceInboxItemID := item.ID
extraDocument := models.SaDocumentTree{
ProjectID: item.ProjectID, CreatedBy: fixture.owner.ID, SourceInboxItemID: &sourceInboxItemID,
ProjectID: item.ProjectID, OwnerID: fixture.owner.ID, SourceInboxItemID: &sourceInboxItemID,
Kind: models.DocumentKindFile, Name: "后续关联资料.md", NormalizedName: "后续关联资料.md",
Extension: ".md", Revision: 1,
}

View File

@@ -127,8 +127,8 @@ func TestWorkspaceMatchesFrontendContract(t *testing.T) {
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Confirm IA", Description: "Review channel layout.", Status: "open", DueAt: &nextRun}).Error)
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Archive notes", Description: "Move outdated notes.", Status: "done"}).Error)
require.NoError(t, database.Create(&models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI prototype critique", Context: "Discuss channel behavior."}).Error)
require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", Revision: 1}).Error)
require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", Revision: 1}).Error)
require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", Revision: 1}).Error)
require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", Revision: 1}).Error)
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap-a1", SortOrder: 7}).Error)
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: other.ID, Title: "Other Board", Icon: "link", URL: "https://example.com/other", SortOrder: 7}).Error)
require.NoError(t, database.Create(&models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly inbox review reminder", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, LastResult: "Not run yet"}).Error)

View File

@@ -33,8 +33,8 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
inbox := models.SaInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn research into tasks.", Status: "open", CreatedAt: createdAt, UpdatedAt: updatedAt}
task := models.SaTask{ProjectID: project.ID, CreatedBy: 1, TagID: &projectTag.ID, Title: "Confirm IA", Description: "Review channel layout.", Status: "done", DueAt: &nextRun, CreatedAt: createdAt, UpdatedAt: updatedAt}
session := models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI critique", Context: "Discuss channel behavior.", CreatedAt: createdAt, UpdatedAt: updatedAt}
firstDocument := models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", MimeType: "text/markdown", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt}
secondDocument := models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", MimeType: "application/pdf", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt}
firstDocument := models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", MimeType: "text/markdown", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt}
secondDocument := models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", MimeType: "application/pdf", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt}
channel := models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap", SortOrder: 7, CreatedAt: createdAt, UpdatedAt: updatedAt}
plan := models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly review", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, CreatedAt: createdAt, UpdatedAt: updatedAt}
for _, record := range []any{&inbox, &task, &session, &firstDocument, &secondDocument, &channel, &plan} {

View File

@@ -116,7 +116,7 @@ func createPostgresSearchDocument(
t.Helper()
document.ProjectID = project.ID
document.ProjectIdentity = project.Identity
document.CreatedBy = ownerID
document.OwnerID = ownerID
document.Kind = models.DocumentKindFile
document.Extension = ".md"
document.MimeType = "text/markdown; charset=utf-8"

View File

@@ -55,7 +55,7 @@ func TestSearchAppliesStableFairGlobalLimitAcrossResultTypes(t *testing.T) {
document := models.SaDocumentTree{
Identity: fmt.Sprintf("00000000-0000-7003-8000-%012d", index),
ProjectID: project.ID,
CreatedBy: 7,
OwnerID: 7,
Kind: models.DocumentKindFile,
Name: fmt.Sprintf("稳定排序文档 %02d.md", index),
NormalizedName: fmt.Sprintf("稳定排序文档 %02d.md", index),
@@ -139,7 +139,7 @@ func TestSearchOnlyReturnsNavigableProjectTaskAndDocumentResults(t *testing.T) {
func createSearchDocument(t *testing.T, database *gorm.DB, projectID, userID uint, name, markdown, identity string, updatedAt time.Time) models.SaDocumentTree {
t.Helper()
document := models.SaDocumentTree{
Identity: identity, ProjectID: projectID, CreatedBy: userID, Kind: models.DocumentKindFile,
Identity: identity, ProjectID: projectID, OwnerID: userID, Kind: models.DocumentKindFile,
Name: name, NormalizedName: strings.ToLower(name), Extension: ".md",
MimeType: "text/markdown", Size: int64(len([]byte(markdown))), Revision: 1,
UpdatedAt: updatedAt,

View File

@@ -29,7 +29,7 @@ func TestPostgresMovePreventsOldProjectShareFromBeingInsertedConcurrently(t *tes
require.NoError(t, database.Create(&first).Error)
require.NoError(t, database.Create(&second).Error)
task := models.SaTask{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Move", Status: "open"}
document := models.SaDocumentTree{ProjectID: first.ID, CreatedBy: owner.ID, Kind: models.DocumentKindFile, Name: "Old context.md", NormalizedName: "old context.md", Extension: ".md", Revision: 1}
document := models.SaDocumentTree{ProjectID: first.ID, OwnerID: owner.ID, Kind: models.DocumentKindFile, Name: "Old context.md", NormalizedName: "old context.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&task).Error)
require.NoError(t, database.Create(&document).Error)
t.Cleanup(func() {

View File

@@ -50,7 +50,7 @@ func TestTaskRegistrarMovesTaskByIdentityAndClearsForeignProjectTag(t *testing.T
require.NoError(t, database.Create(&tag).Error)
task := models.SaTask{ProjectID: first.ID, CreatedBy: 1, TagID: &tag.ID, Title: "迁移任务", Status: "open"}
require.NoError(t, database.Create(&task).Error)
document := models.SaDocumentTree{ProjectID: first.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "旧项目资料.md", NormalizedName: "旧项目资料.md", Extension: ".md", Revision: 1}
document := models.SaDocumentTree{ProjectID: first.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "旧项目资料.md", NormalizedName: "旧项目资料.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&document).Error)
require.NoError(t, NewService(database).ShareObject(task.ID, "document", document.ID))
body := bytes.NewBufferString(fmt.Sprintf(`{"title":"迁移任务","description":"已移动","completed":false,"nextProjectId":%q}`, strings.ToUpper(second.Identity)))

View File

@@ -24,7 +24,7 @@ func TestUpdateLocksTaskBeforeProjectValidation(t *testing.T) {
func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) {
database, _, project, task := newTaskLockFixture(t)
document := models.SaDocumentTree{ProjectID: project.ID, CreatedBy: task.CreatedBy, Kind: models.DocumentKindFile, Name: "Context.md", NormalizedName: "context.md", Extension: ".md", Revision: 1}
document := models.SaDocumentTree{ProjectID: project.ID, OwnerID: task.CreatedBy, Kind: models.DocumentKindFile, Name: "Context.md", NormalizedName: "context.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&document).Error)
queries := captureTaskQueryOrder(t, database)
@@ -39,7 +39,7 @@ func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) {
database := newTestDB(t)
assigneeID := uint(2)
task := models.SaTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"}
document := models.SaDocumentTree{ProjectID: 1, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "合同背景.md", NormalizedName: "合同背景.md", Extension: ".md", Revision: 1}
document := models.SaDocumentTree{ProjectID: 1, OwnerID: 1, Kind: models.DocumentKindFile, Name: "合同背景.md", NormalizedName: "合同背景.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&task).Error)
require.NoError(t, database.Create(&document).Error)
service := NewService()
@@ -68,7 +68,7 @@ func TestShareObjectRejectsUnsupportedType(t *testing.T) {
func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) {
database := newTestDB(t)
task := models.SaTask{ProjectID: 1, CreatedBy: 1, Title: "Review"}
document := models.SaDocumentTree{ProjectID: 2, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Other project.md", NormalizedName: "other project.md", Extension: ".md", Revision: 1}
document := models.SaDocumentTree{ProjectID: 2, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Other project.md", NormalizedName: "other project.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&task).Error)
require.NoError(t, database.Create(&document).Error)
service := NewService()

View File

@@ -19,8 +19,8 @@ type SaDocumentTree struct {
ParentID *uint `gorm:"index"`
ParentIdentity *string `gorm:"type:char(36);index"`
ParentScope string `gorm:"type:char(36);not null;default:'';uniqueIndex:uidx_sa_document_sibling,priority:2"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
OwnerID uint `gorm:"index;not null"`
OwnerIdentity string `gorm:"type:char(36);index"`
SourceInboxItemID *uint `gorm:"index"`
SourceInboxItemIdentity *string `gorm:"type:char(36);index"`
Kind string `gorm:"size:16;not null"`

View File

@@ -82,7 +82,7 @@ func (m *SaDocumentTree) BeforeCreate(tx *gorm.DB) error {
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
if err := resolveIdentity(tx, &SaUser{}, m.OwnerID, &m.OwnerIdentity); err != nil {
return err
}
if err := resolveOptionalIdentity(tx, &SaDocumentTree{}, m.ParentID, &m.ParentIdentity); err != nil {

View File

@@ -44,5 +44,21 @@ func renameLegacyTables(database *gorm.DB) error {
return fmt.Errorf("rename legacy table %s to %s: %w", table.legacy, table.current, err)
}
}
if migrator.HasTable("sa_documents") {
for _, column := range []struct {
legacy string
current string
}{
{legacy: "created_by", current: "owner_id"},
{legacy: "created_by_identity", current: "owner_identity"},
} {
if !migrator.HasColumn("sa_documents", column.legacy) || migrator.HasColumn("sa_documents", column.current) {
continue
}
if err := migrator.RenameColumn("sa_documents", column.legacy, column.current); err != nil {
return fmt.Errorf("rename sa_documents column %s to %s: %w", column.legacy, column.current, err)
}
}
}
return nil
}

View File

@@ -87,6 +87,36 @@ func TestAutoMigrateRejectsAmbiguousLegacyAndCurrentTables(t *testing.T) {
require.EqualError(t, err, "cannot rename legacy table senlin_agent_users: target table sa_users already exists")
}
func TestRenameLegacyTablesRenamesDocumentOwnerColumns(t *testing.T) {
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, database.Exec(`
CREATE TABLE sa_documents (
id integer primary key,
created_by integer not null,
created_by_identity text
)
`).Error)
require.NoError(t, database.Exec(`
INSERT INTO sa_documents (id, created_by, created_by_identity)
VALUES (1, 7, 'owner-identity')
`).Error)
require.NoError(t, renameLegacyTables(database))
require.False(t, database.Migrator().HasColumn("sa_documents", "created_by"))
require.False(t, database.Migrator().HasColumn("sa_documents", "created_by_identity"))
require.True(t, database.Migrator().HasColumn("sa_documents", "owner_id"))
require.True(t, database.Migrator().HasColumn("sa_documents", "owner_identity"))
var stored struct {
OwnerID uint
OwnerIdentity string
}
require.NoError(t, database.Table("sa_documents").Where("id = ?", 1).Take(&stored).Error)
require.Equal(t, uint(7), stored.OwnerID)
require.Equal(t, "owner-identity", stored.OwnerIdentity)
}
func projectIdentifiers(projects []SaProject) []string {
result := make([]string, 0, len(projects))
for _, project := range projects {

View File

@@ -266,7 +266,7 @@ func firstOrCreateDocument(tx *gorm.DB, projectID, userID uint, name, markdown s
return err
}
document = models.SaDocumentTree{
ProjectID: projectID, CreatedBy: userID, Kind: models.DocumentKindFile,
ProjectID: projectID, OwnerID: userID, Kind: models.DocumentKindFile,
Name: name, NormalizedName: strings.ToLower(name), Extension: ".md",
MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(markdown))),
Revision: 1, ScanStatus: "not_scanned",