package notes import ( "archive/zip" "fmt" "io" "path/filepath" "strings" "time" "github.com/google/uuid" "gorm.io/gorm" ) // Service handles note operations type Service struct { db *gorm.DB } // NewService creates a new note service func NewService(db *gorm.DB) *Service { return &Service{db: db} } // Create creates a new note func (s *Service) Create(note *Note) error { note.ID = uuid.New().String() err := s.db.Transaction(func(tx *gorm.DB) error { // Create the note if err := tx.Create(note).Error; err != nil { return fmt.Errorf("failed to create note: %w", err) } // Update links in this note if err := note.UpdateLinks(tx); err != nil { return fmt.Errorf("failed to update note links: %w", err) } // Find and update notes that link to this note's title var notesToUpdate []Note if err := tx.Where("content LIKE ?", "%[["+note.Title+"]]%").Find(¬esToUpdate).Error; err != nil { return fmt.Errorf("failed to find notes linking to %q: %w", note.Title, err) } for _, n := range notesToUpdate { if err := n.UpdateLinks(tx); err != nil { return fmt.Errorf("failed to update links in note %q: %w", n.Title, err) } } return nil }) if err != nil { return err } // Load the note with its relationships if err := s.db.Preload("LinksTo").First(note).Error; err != nil { return fmt.Errorf("failed to load note relationships: %w", err) } return nil } // Get retrieves a note by ID func (s *Service) Get(id string) (*Note, error) { var note Note if err := s.db. Preload("LinksTo", func(db *gorm.DB) *gorm.DB { return db.Select("id", "title") }). Preload("LinkedBy", func(db *gorm.DB) *gorm.DB { return db.Select("id", "title") }). First(¬e, "id = ?", id).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, fmt.Errorf("failed to get note: %w", err) } return ¬e, nil } // List retrieves all notes func (s *Service) List() ([]Note, error) { var notes []Note if err := s.db. Preload("LinksTo", func(db *gorm.DB) *gorm.DB { return db.Select("id", "title") }). Preload("LinkedBy", func(db *gorm.DB) *gorm.DB { return db.Select("id", "title") }). Order("updated_at desc"). Find(¬es).Error; err != nil { return nil, fmt.Errorf("failed to list notes: %w", err) } return notes, nil } // Update updates a note func (s *Service) Update(id string, updates map[string]interface{}) error { return s.db.Transaction(func(tx *gorm.DB) error { // Update the note if err := tx.Model(&Note{}).Where("id = ?", id).Updates(updates).Error; err != nil { return fmt.Errorf("failed to update note: %w", err) } // Load the updated note for link processing var note Note if err := tx.First(¬e, "id = ?", id).Error; err != nil { return fmt.Errorf("failed to load note: %w", err) } // Update links if err := note.UpdateLinks(tx); err != nil { return fmt.Errorf("failed to update note links: %w", err) } return nil }) } // Delete deletes a note func (s *Service) Delete(id string) error { if err := s.db.Delete(&Note{}, "id = ?", id).Error; err != nil { return fmt.Errorf("failed to delete note: %w", err) } return nil } // ImportObsidianVault imports notes from an Obsidian vault zip file func (s *Service) ImportObsidianVault(zipReader *zip.Reader) (int, error) { // Map to store file paths and their content noteFiles := make(map[string]string) // First pass: extract all markdown files for _, file := range zipReader.File { // Skip directories and non-markdown files if file.FileInfo().IsDir() || !strings.HasSuffix(strings.ToLower(file.Name), ".md") { continue } // Open the file rc, err := file.Open() if err != nil { return 0, fmt.Errorf("failed to open file %s: %w", file.Name, err) } // Read the content content, err := io.ReadAll(rc) if err != nil { if err := rc.Close(); err != nil { return 0, fmt.Errorf("failed to close file %s: %w", file.Name, err) } return 0, fmt.Errorf("failed to read file %s: %w", file.Name, err) } if err := rc.Close(); err != nil { return 0, fmt.Errorf("failed to close file %s: %w", file.Name, err) } // Store the content noteFiles[file.Name] = string(content) } // Map to store created notes by their original filename createdNotes := make(map[string]*Note) // Second pass: create notes without links for filePath, content := range noteFiles { // Extract title from filename fileName := filepath.Base(filePath) title := strings.TrimSuffix(fileName, filepath.Ext(fileName)) // Create note note := &Note{ ID: uuid.New().String(), Title: title, Content: content, CreatedAt: time.Now(), UpdatedAt: time.Now(), } // Save note to database if err := s.db.Create(note).Error; err != nil { return len(createdNotes), fmt.Errorf("failed to create note %s: %w", title, err) } // Store created note createdNotes[filePath] = note } // Third pass: update links between notes for filePath, note := range createdNotes { if err := s.db.Transaction(func(tx *gorm.DB) error { // Load the note if err := tx.First(note, "id = ?", note.ID).Error; err != nil { return fmt.Errorf("failed to load note %s: %w", note.Title, err) } // Update links if err := note.UpdateLinks(tx); err != nil { return fmt.Errorf("failed to update links in note %s: %w", note.Title, err) } return nil }); err != nil { return len(createdNotes), fmt.Errorf("failed to update links for note %s: %w", filePath, err) } } return len(createdNotes), nil } // Reset deletes all notes (for testing) func (s *Service) Reset() error { if err := s.db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&Note{}).Error; err != nil { return fmt.Errorf("failed to reset notes: %w", err) } return nil }