feat(documents): rebuild project document workbench
This commit is contained in:
85
backend/internal/models/document.go
Normal file
85
backend/internal/models/document.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
DocumentKindFolder = "folder"
|
||||
DocumentKindFile = "file"
|
||||
)
|
||||
|
||||
type SaDocument struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null;uniqueIndex:uidx_sa_document_sibling,priority:1"`
|
||||
ProjectIdentity string `gorm:"type:char(36);index"`
|
||||
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"`
|
||||
SourceInboxItemID *uint `gorm:"index"`
|
||||
SourceInboxItemIdentity *string `gorm:"type:char(36);index"`
|
||||
Kind string `gorm:"size:16;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
NormalizedName string `gorm:"not null;uniqueIndex:uidx_sa_document_sibling,priority:3"`
|
||||
Extension string `gorm:"size:32"`
|
||||
MimeType string `gorm:"size:255"`
|
||||
Size int64 `gorm:"not null;default:0"`
|
||||
Revision uint64 `gorm:"not null;default:1"`
|
||||
SortOrder int `gorm:"not null;default:0"`
|
||||
ScanStatus string `gorm:"size:24;not null;default:'not_scanned'"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
func (SaDocument) TableName() string {
|
||||
return "sa_documents"
|
||||
}
|
||||
|
||||
type SaDocumentContent struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
DocumentID uint `gorm:"uniqueIndex;not null"`
|
||||
Markdown string `gorm:"type:text"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SaDocumentContent) TableName() string {
|
||||
return "sa_document_contents"
|
||||
}
|
||||
|
||||
type SaDocumentBlob struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
DocumentID uint `gorm:"uniqueIndex;not null"`
|
||||
StorageKey string `gorm:"size:64;uniqueIndex;not null"`
|
||||
RelativePath string `gorm:"not null"`
|
||||
OriginalName string `gorm:"not null"`
|
||||
Checksum string `gorm:"size:64;not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SaDocumentBlob) TableName() string {
|
||||
return "sa_document_blobs"
|
||||
}
|
||||
|
||||
type SaDocumentShare struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
DocumentID uint `gorm:"index;not null"`
|
||||
DocumentIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
TokenHash string `gorm:"size:64;uniqueIndex;not null"`
|
||||
ExpiresAt time.Time `gorm:"index;not null"`
|
||||
RevokedAt *time.Time `gorm:"index"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (SaDocumentShare) TableName() string {
|
||||
return "sa_document_shares"
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func (m *SaTask) BeforeCreate(tx *gorm.DB) error {
|
||||
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
}
|
||||
|
||||
func (m *SaNote) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaDocument) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -85,20 +85,20 @@ func (m *SaNote) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveOptionalIdentity(tx, &SaDocument{}, m.ParentID, &m.ParentIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
}
|
||||
|
||||
func (m *SaSource) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaDocumentShare) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaDocument{}, m.DocumentID, &m.DocumentIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SaAISession) BeforeCreate(tx *gorm.DB) error {
|
||||
@@ -233,10 +233,8 @@ func resolveEntityIdentity(tx *gorm.DB, entityType string, entityID uint, identi
|
||||
return resolveIdentity(tx, &SaInboxItem{}, entityID, identity)
|
||||
case "task":
|
||||
return resolveIdentity(tx, &SaTask{}, entityID, identity)
|
||||
case "note":
|
||||
return resolveIdentity(tx, &SaNote{}, entityID, identity)
|
||||
case "source":
|
||||
return resolveIdentity(tx, &SaSource{}, entityID, identity)
|
||||
case "document":
|
||||
return resolveIdentity(tx, &SaDocument{}, entityID, identity)
|
||||
case "ai_session":
|
||||
return resolveIdentity(tx, &SaAISession{}, entityID, identity)
|
||||
case "tag":
|
||||
|
||||
@@ -31,6 +31,7 @@ func runVersionedMigrations(database *gorm.DB) error {
|
||||
migrations := []versionedMigration{
|
||||
{version: 1, name: "normalize_project_identifiers", run: normalizeLegacyProjectIdentifiers},
|
||||
{version: 2, name: "deduplicate_project_tags", run: deduplicateLegacyProjectTags},
|
||||
{version: 3, name: "replace_notes_sources_with_documents", run: replaceNotesSourcesWithDocuments},
|
||||
}
|
||||
for _, migration := range migrations {
|
||||
// Connection callbacks can provide an initialized Gorm session. Start each
|
||||
@@ -63,6 +64,28 @@ func runVersionedMigrations(database *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceNotesSourcesWithDocuments(tx *gorm.DB) (map[string]int, error) {
|
||||
deletedShares := int64(0)
|
||||
if tx.Migrator().HasTable(&SaTaskShare{}) {
|
||||
result := tx.Where("object_type IN ?", []string{"note", "source"}).Delete(&SaTaskShare{})
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
deletedShares = result.RowsAffected
|
||||
}
|
||||
dropped := 0
|
||||
for _, table := range []string{"sa_notes", "sa_sources"} {
|
||||
if !tx.Migrator().HasTable(table) {
|
||||
continue
|
||||
}
|
||||
if err := tx.Migrator().DropTable(table); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dropped++
|
||||
}
|
||||
return map[string]int{"dropped_tables": dropped, "deleted_task_shares": int(deletedShares)}, nil
|
||||
}
|
||||
|
||||
func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) {
|
||||
var projects []SaProject
|
||||
if err := tx.Model(&SaProject{}).Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil {
|
||||
|
||||
@@ -63,14 +63,14 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
|
||||
|
||||
var migrationCount int64
|
||||
require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error)
|
||||
require.Equal(t, int64(2), migrationCount)
|
||||
require.Equal(t, int64(3), migrationCount)
|
||||
var auditDetails []string
|
||||
require.NoError(t, database.Table("sa_schema_migrations").Order("version asc").Pluck("details", &auditDetails).Error)
|
||||
require.Contains(t, auditDetails[0], `"updated":3`)
|
||||
require.Contains(t, auditDetails[1], `"deduplicated":1`)
|
||||
require.NoError(t, AutoMigrate(database), "versioned migrations must be safe to run again")
|
||||
require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error)
|
||||
require.Equal(t, int64(2), migrationCount)
|
||||
require.Equal(t, int64(3), migrationCount)
|
||||
|
||||
require.Error(t, database.Exec(`INSERT INTO sa_projects (owner_id, name, identifier) VALUES (7, 'still duplicate', 'DUP')`).Error)
|
||||
require.Error(t, database.Exec(`INSERT INTO sa_tags (project_id, name) VALUES (1, 'UI')`).Error)
|
||||
|
||||
@@ -36,8 +36,10 @@ func AutoMigrate(database *gorm.DB) error {
|
||||
&SaInboxItem{},
|
||||
&SaInboxSuggestion{},
|
||||
&SaTask{},
|
||||
&SaNote{},
|
||||
&SaSource{},
|
||||
&SaDocument{},
|
||||
&SaDocumentContent{},
|
||||
&SaDocumentBlob{},
|
||||
&SaDocumentShare{},
|
||||
&SaAIExpertCategory{},
|
||||
&SaAIExpertItem{},
|
||||
&SaAISession{},
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SaNote struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
ProjectIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
CreatedByIdentity string `gorm:"type:char(36);index"`
|
||||
SourceInboxItemID *uint `gorm:"index"`
|
||||
SourceInboxItemIdentity *string `gorm:"type:char(36);index"`
|
||||
Title string `gorm:"not null"`
|
||||
Markdown string `gorm:"type:text"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SaNote) TableName() string {
|
||||
return "sa_notes"
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SaSource struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
ProjectIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
CreatedByIdentity string `gorm:"type:char(36);index"`
|
||||
SourceInboxItemID *uint `gorm:"index"`
|
||||
SourceInboxItemIdentity *string `gorm:"type:char(36);index"`
|
||||
Kind string `gorm:"not null"`
|
||||
Title string `gorm:"not null"`
|
||||
URL string
|
||||
FilePath string
|
||||
ContentText string `gorm:"type:text"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SaSource) TableName() string {
|
||||
return "sa_sources"
|
||||
}
|
||||
Reference in New Issue
Block a user