package database import ( "crypto/sha256" "encoding/hex" "errors" "time" "gorm.io/gorm" ) var ( ErrUnauthorizedOrMissing = errors.New("item not found or unauthorized") ErrVersionNotFound = errors.New("version not found") ErrDeletionFailed = errors.New("deletion failed") ) type Note struct { ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` UserID string `gorm:"type:uuid;not null;index"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` Versions []NoteVersion `gorm:"foreignKey:NoteID;constraint:OnDelete:CASCADE"` } type NoteVersion struct { ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` NoteID string `gorm:"type:uuid;not null;index"` Title string `gorm:"type:varchar(255);not null"` Content string `gorm:"type:text;not null"` // TOAST will automatically compress at >2KB VersionNumber int `gorm:"not null"` ContentHash string `gorm:"type:varchar(64);not null;index"` CreatedAt time.Time `gorm:"autoCreateTime"` } // Helper hook that runs automatically before a new note version is created. It calculates a // composite SHA256 hash of the title-content combination and looks up thte valid version // number based on any previous entries in the table. func (nv *NoteVersion) BeforeCreate(tx *gorm.DB) (err error) { combined := nv.Title + "\n" + nv.Content hash := sha256.Sum256([]byte(combined)) nv.ContentHash = hex.EncodeToString(hash[:]) var maxVersion int err = tx.Model(&NoteVersion{}). Where("note_id = ?", nv.NoteID). Select("COALESCE(MAX(version_number), 0)"). Scan(&maxVersion). Error if err != nil { return err } nv.VersionNumber = maxVersion + 1 return nil } // NOTE: User ID should always be injected to the requests by a middleware (and not taken from // a user input). // Create a new note and its initial version. Might return an error if the creation transaction // fails. On success returns a pointer to the newly created note object. func CreateNote(db *gorm.DB, userID string, title string, content string) (*Note, error) { note := Note{ UserID: userID, } err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(¬e).Error; err != nil { return err } return tx.Create(&NoteVersion{ NoteID: note.ID, Title: title, Content: content, }).Error }) return ¬e, err } // Get a list of `limit` notes belonging to the given user and their historical versions (preloaded // version numbers). func ListNotes(db *gorm.DB, userID string, page int, limit int) ([]Note, error) { var notes []Note offset := (page - 1) * limit err := db. Preload("Versions", func(db *gorm.DB) *gorm.DB { return db.Order("note_versions.version_number DESC").Limit(1) }). Where("user_id = ?", userID). Order("created_at DESC"). Offset(offset). Limit(limit). Find(¬es). Error return notes, err } // Get a single note with its preloaded version numbers. func GetNote(db *gorm.DB, userID string, noteID string) (*Note, error) { var note Note err := db. Preload("Versions", func(db *gorm.DB) *gorm.DB { return db.Order("note_versions.version_number DESC") }). Where("id = ? AND user_id = ?", noteID, userID). First(¬e). Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUnauthorizedOrMissing } return ¬e, err } // Get all the linked historical versions of the given note. func GetNoteVersions(db *gorm.DB, userID string, noteID string, page int, limit int) ([]NoteVersion, error) { var versions []NoteVersion var note Note if err := db. Select("id"). Where("id = ? AND user_id = ?", noteID, userID). First(¬e). Error; err != nil { return nil, ErrUnauthorizedOrMissing } offset := (page - 1) * limit err := db. Where("note_id = ?", noteID). Order("version_number DESC"). Offset(offset). Limit(limit). Find(&versions). Error return versions, err } // Get a specific historical version of the given note. func GetNoteVersion(db *gorm.DB, userID string, noteID string, versionNumber int) (*NoteVersion, error) { var version NoteVersion // Ownership check via note var note Note if err := db. Select("id"). Where("id = ? AND user_id = ?", noteID, userID). First(¬e). Error; err != nil { return nil, ErrUnauthorizedOrMissing } err := db. Where("note_id = ? AND version_number = ?", noteID, versionNumber). First(&version). Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrVersionNotFound } return &version, err } // Update an existing note by creating a new version. If the composite SHA256 hash of the new // version contents matches with an already stored version, the new version will be skipped. func UpdateNote(db *gorm.DB, userID string, noteID string, newTitle string, newContent string) error { return db.Transaction(func(tx *gorm.DB) error { var note Note if err := tx. Where("id = ? AND user_id = ?", noteID, userID). First(¬e). Error; err != nil { return ErrUnauthorizedOrMissing } var latest NoteVersion if err := tx. Where("note_id = ?", noteID). Order("version_number DESC"). First(&latest). Error; err != nil { return err } combined := newTitle + "\n" + newContent newHash := sha256.Sum256([]byte(combined)) if hex.EncodeToString(newHash[:]) == latest.ContentHash { return nil // No changes } return tx.Create(&NoteVersion{ NoteID: noteID, Title: newTitle, Content: newContent, }).Error }) } // Remove a note and its versions permanently (hard delete). func DeleteNote(db *gorm.DB, userID string, noteID string) error { return db.Transaction(func(tx *gorm.DB) error { var note Note if err := tx. Where("id = ? AND user_id = ?", noteID, userID). First(¬e). Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrUnauthorizedOrMissing } return err } // Delete note (cascades to versions) if err := tx.Delete(¬e).Error; err != nil { return ErrDeletionFailed } return nil }) }