package database import ( "context" "errors" "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" "gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/entities" ) // NoteRepository implements the note repository interface type NoteRepository struct { collection *mongo.Collection } func notePrioritySortOptions(skip, limit int) *options.FindOptionsBuilder { return options.Find(). SetSkip(int64(skip)). SetLimit(int64(limit)). SetSort(bson.D{ {Key: "is_pinned", Value: -1}, {Key: "is_favorite", Value: -1}, {Key: "title", Value: 1}, }). SetCollation(&options.Collation{Locale: "en", Strength: 2}) } // NewNoteRepository creates a new note repository func NewNoteRepository(db *mongo.Database) *NoteRepository { return &NoteRepository{ collection: db.Collection("notes"), } } // CreateNote creates a new note func (r *NoteRepository) CreateNote(ctx context.Context, note *entities.Note) error { note.ID = bson.NewObjectID() note.CreatedAt = time.Now() note.UpdatedAt = time.Now() _, err := r.collection.InsertOne(ctx, note) return err } // GetNoteByID retrieves a note by ID func (r *NoteRepository) GetNoteByID(ctx context.Context, id bson.ObjectID) (*entities.Note, error) { var note entities.Note err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(¬e) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("note not found") } return nil, err } return ¬e, nil } // GetNotesBySpaceID retrieves all notes in a space with pagination func (r *NoteRepository) GetNotesBySpaceID(ctx context.Context, spaceID bson.ObjectID, skip, limit int) ([]*entities.Note, error) { var notes []*entities.Note opts := notePrioritySortOptions(skip, limit) cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID}, opts) if err != nil { return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, ¬es); err != nil { return nil, err } return notes, nil } // GetPublicNotesBySpaceID retrieves public notes in a space with pagination. func (r *NoteRepository) GetPublicNotesBySpaceID(ctx context.Context, spaceID bson.ObjectID, skip, limit int) ([]*entities.Note, error) { var notes []*entities.Note opts := notePrioritySortOptions(skip, limit) cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID, "is_public": true}, opts) if err != nil { return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, ¬es); err != nil { return nil, err } return notes, nil } // GetNotesByCategory retrieves notes in a category func (r *NoteRepository) GetNotesByCategory(ctx context.Context, spaceID, categoryID bson.ObjectID) ([]*entities.Note, error) { var notes []*entities.Note opts := options.Find(). SetSort(bson.D{ {Key: "is_pinned", Value: -1}, {Key: "is_favorite", Value: -1}, {Key: "title", Value: 1}, }). SetCollation(&options.Collation{Locale: "en", Strength: 2}) cursor, err := r.collection.Find(ctx, bson.M{ "space_id": spaceID, "category_id": categoryID, }, opts) if err != nil { return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, ¬es); err != nil { return nil, err } return notes, nil } // SearchNotes performs full-text search on notes func (r *NoteRepository) SearchNotes(ctx context.Context, spaceID bson.ObjectID, query string) ([]*entities.Note, error) { var notes []*entities.Note cursor, err := r.collection.Find(ctx, bson.M{ "space_id": spaceID, "$text": bson.M{ "$search": query, }, }) if err != nil { return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, ¬es); err != nil { return nil, err } return notes, nil } // UpdateNote updates a note func (r *NoteRepository) UpdateNote(ctx context.Context, note *entities.Note) error { note.UpdatedAt = time.Now() _, err := r.collection.ReplaceOne(ctx, bson.M{"_id": note.ID}, note) return err } // DeleteNote deletes a note func (r *NoteRepository) DeleteNote(ctx context.Context, id bson.ObjectID) error { _, err := r.collection.DeleteOne(ctx, bson.M{"_id": id}) return err } // DeleteNotesBySpaceID deletes all notes in a space func (r *NoteRepository) DeleteNotesBySpaceID(ctx context.Context, spaceID bson.ObjectID) error { _, err := r.collection.DeleteMany(ctx, bson.M{"space_id": spaceID}) return err } // EnsureIndexes creates necessary indexes func (r *NoteRepository) EnsureIndexes(ctx context.Context) error { indexModel := []mongo.IndexModel{ { Keys: bson.D{bson.E{Key: "space_id", Value: 1}}, }, { Keys: bson.D{bson.E{Key: "category_id", Value: 1}}, }, { Keys: bson.D{ bson.E{Key: "title", Value: "text"}, bson.E{Key: "content", Value: "text"}, bson.E{Key: "tags", Value: "text"}, }, }, { Keys: bson.D{bson.E{Key: "updated_at", Value: -1}}, }, { Keys: bson.D{ {Key: "space_id", Value: 1}, {Key: "is_pinned", Value: -1}, {Key: "is_favorite", Value: -1}, {Key: "title", Value: 1}, }, }, { Keys: bson.D{ {Key: "space_id", Value: 1}, {Key: "is_public", Value: 1}, {Key: "is_pinned", Value: -1}, {Key: "is_favorite", Value: -1}, {Key: "title", Value: 1}, }, }, } _, err := r.collection.Indexes().CreateMany(ctx, indexModel) return err } // ========== CATEGORY REPOSITORY ========== // CategoryRepository implements the category repository interface type CategoryRepository struct { collection *mongo.Collection } // NewCategoryRepository creates a new category repository func NewCategoryRepository(db *mongo.Database) *CategoryRepository { return &CategoryRepository{ collection: db.Collection("categories"), } } // CreateCategory creates a new category func (r *CategoryRepository) CreateCategory(ctx context.Context, category *entities.Category) error { category.ID = bson.NewObjectID() category.CreatedAt = time.Now() category.UpdatedAt = time.Now() _, err := r.collection.InsertOne(ctx, category) return err } // GetCategoryByID retrieves a category by ID func (r *CategoryRepository) GetCategoryByID(ctx context.Context, id bson.ObjectID) (*entities.Category, error) { var category entities.Category err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&category) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("category not found") } return nil, err } return &category, nil } // GetCategoriesBySpaceID retrieves all categories in a space func (r *CategoryRepository) GetCategoriesBySpaceID(ctx context.Context, spaceID bson.ObjectID) ([]*entities.Category, error) { var categories []*entities.Category opts := options.Find().SetSort(bson.M{"order": 1}) cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID}, opts) if err != nil { return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &categories); err != nil { return nil, err } return categories, nil } // GetRootCategories retrieves root level categories in a space func (r *CategoryRepository) GetRootCategories(ctx context.Context, spaceID bson.ObjectID) ([]*entities.Category, error) { var categories []*entities.Category opts := options.Find().SetSort(bson.M{"order": 1}) cursor, err := r.collection.Find(ctx, bson.M{ "space_id": spaceID, "parent_id": nil, }, opts) if err != nil { return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &categories); err != nil { return nil, err } return categories, nil } // GetSubcategories retrieves subcategories of a category func (r *CategoryRepository) GetSubcategories(ctx context.Context, parentID bson.ObjectID) ([]*entities.Category, error) { var categories []*entities.Category opts := options.Find().SetSort(bson.M{"order": 1}) cursor, err := r.collection.Find(ctx, bson.M{"parent_id": parentID}, opts) if err != nil { return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &categories); err != nil { return nil, err } return categories, nil } // UpdateCategory updates a category func (r *CategoryRepository) UpdateCategory(ctx context.Context, category *entities.Category) error { category.UpdatedAt = time.Now() _, err := r.collection.ReplaceOne(ctx, bson.M{"_id": category.ID}, category) return err } // DeleteCategory deletes a category func (r *CategoryRepository) DeleteCategory(ctx context.Context, id bson.ObjectID) error { _, err := r.collection.DeleteOne(ctx, bson.M{"_id": id}) return err } // DeleteCategoriesBySpaceID deletes all categories in a space func (r *CategoryRepository) DeleteCategoriesBySpaceID(ctx context.Context, spaceID bson.ObjectID) error { _, err := r.collection.DeleteMany(ctx, bson.M{"space_id": spaceID}) return err } // EnsureIndexes creates necessary indexes func (r *CategoryRepository) EnsureIndexes(ctx context.Context) error { indexModel := []mongo.IndexModel{ { Keys: bson.D{bson.E{Key: "space_id", Value: 1}}, }, { Keys: bson.D{bson.E{Key: "parent_id", Value: 1}}, }, { Keys: bson.D{bson.E{Key: "order", Value: 1}}, }, } _, err := r.collection.Indexes().CreateMany(ctx, indexModel) return err }