339 lines
8.9 KiB
Go
339 lines
8.9 KiB
Go
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
|
|
}
|