first commit

This commit is contained in:
domrichardson
2026-03-24 16:03:04 +00:00
commit df40cc57e1
80 changed files with 16766 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
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"
"github.com/noteapp/backend/internal/domain/entities"
)
// AccountRecoveryRepository implements account recovery operations
type AccountRecoveryRepository struct {
collection *mongo.Collection
}
type featureFlagSettings struct {
ID string `bson:"_id"`
Flags entities.FeatureFlags `bson:"flags"`
UpdatedAt time.Time `bson:"updated_at"`
}
// FeatureFlagRepository implements app-wide feature flag operations.
type FeatureFlagRepository struct {
collection *mongo.Collection
}
// NewFeatureFlagRepository creates a new feature flag repository.
func NewFeatureFlagRepository(db *mongo.Database) *FeatureFlagRepository {
return &FeatureFlagRepository{
collection: db.Collection("app_settings"),
}
}
// GetFeatureFlags returns persisted feature flags or defaults when not set.
func (r *FeatureFlagRepository) GetFeatureFlags(ctx context.Context) (*entities.FeatureFlags, error) {
var settings featureFlagSettings
err := r.collection.FindOne(ctx, bson.M{"_id": "feature_flags"}).Decode(&settings)
if err != nil {
if err == mongo.ErrNoDocuments {
return entities.NewDefaultFeatureFlags(), nil
}
return nil, err
}
flags := settings.Flags
return &flags, nil
}
// UpdateFeatureFlags persists feature flags.
func (r *FeatureFlagRepository) UpdateFeatureFlags(ctx context.Context, flags *entities.FeatureFlags) error {
if flags == nil {
flags = entities.NewDefaultFeatureFlags()
}
now := time.Now()
_, err := r.collection.UpdateOne(
ctx,
bson.M{"_id": "feature_flags"},
bson.M{
"$set": bson.M{
"flags": flags,
"updated_at": now,
},
},
options.UpdateOne().SetUpsert(true),
)
return err
}
// NewAccountRecoveryRepository creates a new recovery repository
func NewAccountRecoveryRepository(db *mongo.Database) *AccountRecoveryRepository {
return &AccountRecoveryRepository{
collection: db.Collection("account_recovery"),
}
}
// CreateRecovery creates a new recovery token
func (r *AccountRecoveryRepository) CreateRecovery(ctx context.Context, recovery *entities.AccountRecovery) error {
recovery.ID = bson.NewObjectID()
recovery.CreatedAt = time.Now()
_, err := r.collection.InsertOne(ctx, recovery)
return err
}
// GetRecoveryByToken retrieves a recovery record by token
func (r *AccountRecoveryRepository) GetRecoveryByToken(ctx context.Context, token string) (*entities.AccountRecovery, error) {
var recovery entities.AccountRecovery
err := r.collection.FindOne(ctx, bson.M{
"token": token,
"expires_at": bson.M{"$gt": time.Now()},
"used_at": bson.M{"$exists": false},
}).Decode(&recovery)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("recovery token not found or expired")
}
return nil, err
}
return &recovery, nil
}
// MarkRecoveryUsed marks a recovery token as used
func (r *AccountRecoveryRepository) MarkRecoveryUsed(ctx context.Context, id bson.ObjectID) error {
now := time.Now()
_, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{
"$set": bson.M{"used_at": now},
})
return err
}
// NoteRevisionRepository implements note revision operations
type NoteRevisionRepository struct {
collection *mongo.Collection
}
// NewNoteRevisionRepository creates a new revision repository
func NewNoteRevisionRepository(db *mongo.Database) *NoteRevisionRepository {
return &NoteRevisionRepository{
collection: db.Collection("note_revisions"),
}
}
// CreateRevision creates a new note revision
func (r *NoteRevisionRepository) CreateRevision(ctx context.Context, revision *entities.NoteRevision) error {
revision.ID = bson.NewObjectID()
revision.CreatedAt = time.Now()
_, err := r.collection.InsertOne(ctx, revision)
return err
}
// GetRevisionsByNoteID retrieves all revisions for a note
func (r *NoteRevisionRepository) GetRevisionsByNoteID(ctx context.Context, noteID bson.ObjectID) ([]*entities.NoteRevision, error) {
var revisions []*entities.NoteRevision
cursor, err := r.collection.Find(ctx, bson.M{"note_id": noteID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &revisions); err != nil {
return nil, err
}
return revisions, nil
}
// GetRevisionByID retrieves a specific revision
func (r *NoteRevisionRepository) GetRevisionByID(ctx context.Context, id bson.ObjectID) (*entities.NoteRevision, error) {
var revision entities.NoteRevision
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&revision)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("revision not found")
}
return nil, err
}
return &revision, nil
}
// AuthProviderRepository implements auth provider operations
type AuthProviderRepository struct {
collection *mongo.Collection
}
// NewAuthProviderRepository creates a new provider repository
func NewAuthProviderRepository(db *mongo.Database) *AuthProviderRepository {
return &AuthProviderRepository{
collection: db.Collection("auth_providers"),
}
}
// CreateProvider creates a new provider
func (r *AuthProviderRepository) CreateProvider(ctx context.Context, provider *entities.AuthProvider) error {
provider.ID = bson.NewObjectID()
provider.CreatedAt = time.Now()
provider.UpdatedAt = time.Now()
_, err := r.collection.InsertOne(ctx, provider)
return err
}
// GetProviderByID retrieves a provider by ID
func (r *AuthProviderRepository) GetProviderByID(ctx context.Context, id bson.ObjectID) (*entities.AuthProvider, error) {
var provider entities.AuthProvider
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&provider)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("provider not found")
}
return nil, err
}
return &provider, nil
}
// GetAllProviders retrieves all active providers
func (r *AuthProviderRepository) GetAllProviders(ctx context.Context) ([]*entities.AuthProvider, error) {
var providers []*entities.AuthProvider
cursor, err := r.collection.Find(ctx, bson.M{"is_active": true})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &providers); err != nil {
return nil, err
}
return providers, nil
}
// UpdateProvider updates a provider
func (r *AuthProviderRepository) UpdateProvider(ctx context.Context, provider *entities.AuthProvider) error {
provider.UpdatedAt = time.Now()
_, err := r.collection.ReplaceOne(ctx, bson.M{"_id": provider.ID}, provider)
return err
}
// DeleteProvider deletes a provider
func (r *AuthProviderRepository) DeleteProvider(ctx context.Context, id bson.ObjectID) error {
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
return err
}
// UserProviderLinkRepository implements user provider link operations
type UserProviderLinkRepository struct {
collection *mongo.Collection
}
// NewUserProviderLinkRepository creates a new link repository
func NewUserProviderLinkRepository(db *mongo.Database) *UserProviderLinkRepository {
return &UserProviderLinkRepository{
collection: db.Collection("user_provider_links"),
}
}
// CreateLink creates a new user provider link
func (r *UserProviderLinkRepository) CreateLink(ctx context.Context, link *entities.UserProviderLink) error {
link.ID = bson.NewObjectID()
link.LinkedAt = time.Now()
_, err := r.collection.InsertOne(ctx, link)
return err
}
// GetLink retrieves a user provider link
func (r *UserProviderLinkRepository) GetLink(ctx context.Context, userID, providerID bson.ObjectID) (*entities.UserProviderLink, error) {
var link entities.UserProviderLink
err := r.collection.FindOne(ctx, bson.M{
"user_id": userID,
"provider_id": providerID,
}).Decode(&link)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("link not found")
}
return nil, err
}
return &link, nil
}
// GetLinkByProviderUserID retrieves a link by provider user ID
func (r *UserProviderLinkRepository) GetLinkByProviderUserID(ctx context.Context, providerID bson.ObjectID, providerUserID string) (*entities.UserProviderLink, error) {
var link entities.UserProviderLink
err := r.collection.FindOne(ctx, bson.M{
"provider_id": providerID,
"provider_user_id": providerUserID,
}).Decode(&link)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("link not found")
}
return nil, err
}
return &link, nil
}
// GetUserLinks retrieves all provider links for a user
func (r *UserProviderLinkRepository) GetUserLinks(ctx context.Context, userID bson.ObjectID) ([]*entities.UserProviderLink, error) {
var links []*entities.UserProviderLink
cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &links); err != nil {
return nil, err
}
return links, nil
}
// UpdateLink updates a provider link
func (r *UserProviderLinkRepository) UpdateLink(ctx context.Context, link *entities.UserProviderLink) error {
_, err := r.collection.ReplaceOne(ctx, bson.M{"_id": link.ID}, link)
return err
}
// DeleteLink deletes a provider link
func (r *UserProviderLinkRepository) DeleteLink(ctx context.Context, id bson.ObjectID) error {
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
return err
}

View File

@@ -0,0 +1,92 @@
package database
import (
"context"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
// Database holds all repository instances
type Database struct {
Client *mongo.Client
DB *mongo.Database
UserRepo *UserRepository
SpaceRepo *SpaceRepository
MembershipRepo *MembershipRepository
NoteRepo *NoteRepository
CategoryRepo *CategoryRepository
RevisionRepo *NoteRevisionRepository
GroupRepo *PermissionGroupRepository
ProviderRepo *AuthProviderRepository
LinkRepo *UserProviderLinkRepository
RecoveryRepo *AccountRecoveryRepository
FeatureFlagRepo *FeatureFlagRepository
}
// NewDatabase initializes a new database connection and repositories
func NewDatabase(ctx context.Context, mongoURL string) (*Database, error) {
client, err := mongo.Connect(options.Client().ApplyURI(mongoURL))
if err != nil {
return nil, err
}
// Verify connection
if err = client.Ping(ctx, nil); err != nil {
return nil, err
}
db := client.Database("noteapp")
// Create repositories
database := &Database{
Client: client,
DB: db,
UserRepo: NewUserRepository(db),
SpaceRepo: NewSpaceRepository(db),
MembershipRepo: NewMembershipRepository(db),
NoteRepo: NewNoteRepository(db),
CategoryRepo: NewCategoryRepository(db),
RevisionRepo: NewNoteRevisionRepository(db),
GroupRepo: NewPermissionGroupRepository(db),
ProviderRepo: NewAuthProviderRepository(db),
LinkRepo: NewUserProviderLinkRepository(db),
RecoveryRepo: NewAccountRecoveryRepository(db),
FeatureFlagRepo: NewFeatureFlagRepository(db),
}
// Ensure all indexes are created
if err := database.EnsureIndexes(ctx); err != nil {
return nil, err
}
return database, nil
}
// EnsureIndexes ensures all necessary indexes are created
func (d *Database) EnsureIndexes(ctx context.Context) error {
if err := d.UserRepo.EnsureIndexes(ctx); err != nil {
return err
}
if err := d.SpaceRepo.EnsureIndexes(ctx); err != nil {
return err
}
if err := d.MembershipRepo.EnsureIndexes(ctx); err != nil {
return err
}
if err := d.NoteRepo.EnsureIndexes(ctx); err != nil {
return err
}
if err := d.CategoryRepo.EnsureIndexes(ctx); err != nil {
return err
}
if err := d.GroupRepo.EnsureIndexes(ctx); err != nil {
return err
}
return nil
}
// Close closes the database connection
func (d *Database) Close(ctx context.Context) error {
return d.Client.Disconnect(ctx)
}

View File

@@ -0,0 +1,129 @@
package database
import (
"context"
"errors"
"strings"
"time"
"github.com/noteapp/backend/internal/domain/entities"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
// PermissionGroupRepository implements permission group data access.
type PermissionGroupRepository struct {
collection *mongo.Collection
}
// NewPermissionGroupRepository creates a new group repository.
func NewPermissionGroupRepository(db *mongo.Database) *PermissionGroupRepository {
return &PermissionGroupRepository{collection: db.Collection("permission_groups")}
}
// CreateGroup creates a new permission group.
func (r *PermissionGroupRepository) CreateGroup(ctx context.Context, group *entities.PermissionGroup) error {
group.ID = bson.NewObjectID()
group.Name = strings.TrimSpace(group.Name)
group.NameKey = strings.ToLower(group.Name)
group.CreatedAt = time.Now()
group.UpdatedAt = time.Now()
_, err := r.collection.InsertOne(ctx, group)
return err
}
// GetGroupByID retrieves a group by ID.
func (r *PermissionGroupRepository) GetGroupByID(ctx context.Context, id bson.ObjectID) (*entities.PermissionGroup, error) {
var group entities.PermissionGroup
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&group)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("group not found")
}
return nil, err
}
return &group, nil
}
// GetGroupByName retrieves a group by case-insensitive name.
func (r *PermissionGroupRepository) GetGroupByName(ctx context.Context, name string) (*entities.PermissionGroup, error) {
normalized := strings.ToLower(strings.TrimSpace(name))
var group entities.PermissionGroup
err := r.collection.FindOne(ctx, bson.M{"name_key": normalized}).Decode(&group)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("group not found")
}
return nil, err
}
return &group, nil
}
// GetGroupsByIDs retrieves groups by IDs.
func (r *PermissionGroupRepository) GetGroupsByIDs(ctx context.Context, ids []bson.ObjectID) ([]*entities.PermissionGroup, error) {
if len(ids) == 0 {
return []*entities.PermissionGroup{}, nil
}
cursor, err := r.collection.Find(ctx, bson.M{"_id": bson.M{"$in": ids}})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var groups []*entities.PermissionGroup
if err := cursor.All(ctx, &groups); err != nil {
return nil, err
}
return groups, nil
}
// ListGroups retrieves all groups sorted by name.
func (r *PermissionGroupRepository) ListGroups(ctx context.Context) ([]*entities.PermissionGroup, error) {
opts := options.Find().SetSort(bson.D{{Key: "name", Value: 1}})
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var groups []*entities.PermissionGroup
if err := cursor.All(ctx, &groups); err != nil {
return nil, err
}
return groups, nil
}
// UpdateGroup updates an existing group.
func (r *PermissionGroupRepository) UpdateGroup(ctx context.Context, group *entities.PermissionGroup) error {
group.UpdatedAt = time.Now()
_, err := r.collection.UpdateOne(ctx, bson.M{"_id": group.ID}, bson.M{
"$set": bson.M{
"name": strings.TrimSpace(group.Name),
"name_key": strings.ToLower(strings.TrimSpace(group.Name)),
"description": group.Description,
"permissions": group.Permissions,
"is_system": group.IsSystem,
"updated_at": group.UpdatedAt,
},
})
return err
}
// DeleteGroup deletes a group.
func (r *PermissionGroupRepository) DeleteGroup(ctx context.Context, id bson.ObjectID) error {
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
return err
}
// EnsureIndexes creates indexes for the permission groups collection.
func (r *PermissionGroupRepository) EnsureIndexes(ctx context.Context) error {
_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
{
Keys: bson.D{{Key: "name_key", Value: 1}},
Options: options.Index().SetUnique(true),
},
})
return err
}

View File

@@ -0,0 +1,338 @@
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"
"github.com/noteapp/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(&note)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("note not found")
}
return nil, err
}
return &note, 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, &notes); 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, &notes); 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, &notes); 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, &notes); 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
}

View File

@@ -0,0 +1,249 @@
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"
"github.com/noteapp/backend/internal/domain/entities"
)
// SpaceRepository implements the space repository interface
type SpaceRepository struct {
collection *mongo.Collection
}
// NewSpaceRepository creates a new space repository
func NewSpaceRepository(db *mongo.Database) *SpaceRepository {
return &SpaceRepository{
collection: db.Collection("spaces"),
}
}
// CreateSpace creates a new space
func (r *SpaceRepository) CreateSpace(ctx context.Context, space *entities.Space) error {
space.ID = bson.NewObjectID()
space.CreatedAt = time.Now()
space.UpdatedAt = time.Now()
_, err := r.collection.InsertOne(ctx, space)
return err
}
// GetSpaceByID retrieves a space by ID
func (r *SpaceRepository) GetSpaceByID(ctx context.Context, id bson.ObjectID) (*entities.Space, error) {
var space entities.Space
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&space)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("space not found")
}
return nil, err
}
return &space, nil
}
// GetSpacesByUserID retrieves all spaces for a user (via memberships)
func (r *SpaceRepository) GetSpacesByUserID(ctx context.Context, userID bson.ObjectID) ([]*entities.Space, error) {
var spaces []*entities.Space
// Query spaces where user is a member
opts := options.Find().SetSort(bson.M{"created_at": -1})
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
// This would typically be joined with membership collection
// For now, returning all spaces - in production, filter by membership
if err = cursor.All(ctx, &spaces); err != nil {
return nil, err
}
return spaces, nil
}
// UpdateSpace updates a space
func (r *SpaceRepository) UpdateSpace(ctx context.Context, space *entities.Space) error {
space.UpdatedAt = time.Now()
_, err := r.collection.ReplaceOne(ctx, bson.M{"_id": space.ID}, space)
return err
}
// DeleteSpace deletes a space
func (r *SpaceRepository) DeleteSpace(ctx context.Context, id bson.ObjectID) error {
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
return err
}
// GetAllSpaces retrieves all spaces sorted by creation date descending
func (r *SpaceRepository) GetAllSpaces(ctx context.Context) ([]*entities.Space, error) {
opts := options.Find().SetSort(bson.D{bson.E{Key: "created_at", Value: -1}})
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var spaces []*entities.Space
if err := cursor.All(ctx, &spaces); err != nil {
return nil, err
}
return spaces, nil
}
// GetPublicSpaces retrieves all spaces marked as public
func (r *SpaceRepository) GetPublicSpaces(ctx context.Context) ([]*entities.Space, error) {
opts := options.Find().SetSort(bson.D{bson.E{Key: "created_at", Value: -1}})
cursor, err := r.collection.Find(ctx, bson.M{"is_public": true}, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var spaces []*entities.Space
if err := cursor.All(ctx, &spaces); err != nil {
return nil, err
}
return spaces, nil
}
// EnsureIndexes creates necessary indexes
func (r *SpaceRepository) EnsureIndexes(ctx context.Context) error {
indexModel := []mongo.IndexModel{
{
Keys: bson.D{bson.E{Key: "owner_id", Value: 1}},
},
{
Keys: bson.D{bson.E{Key: "created_at", Value: -1}},
},
}
_, err := r.collection.Indexes().CreateMany(ctx, indexModel)
return err
}
// ========== MEMBERSHIP REPOSITORY ==========
// MembershipRepository implements the membership repository interface
type MembershipRepository struct {
collection *mongo.Collection
}
// NewMembershipRepository creates a new membership repository
func NewMembershipRepository(db *mongo.Database) *MembershipRepository {
return &MembershipRepository{
collection: db.Collection("memberships"),
}
}
// CreateMembership creates a new membership
func (r *MembershipRepository) CreateMembership(ctx context.Context, membership *entities.Membership) error {
membership.ID = bson.NewObjectID()
membership.JoinedAt = time.Now()
_, err := r.collection.InsertOne(ctx, membership)
return err
}
// GetMembershipByID retrieves a membership by ID
func (r *MembershipRepository) GetMembershipByID(ctx context.Context, id bson.ObjectID) (*entities.Membership, error) {
var membership entities.Membership
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&membership)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("membership not found")
}
return nil, err
}
return &membership, nil
}
// GetUserMembership retrieves a membership for a user in a space
func (r *MembershipRepository) GetUserMembership(ctx context.Context, userID, spaceID bson.ObjectID) (*entities.Membership, error) {
var membership entities.Membership
err := r.collection.FindOne(ctx, bson.M{
"user_id": userID,
"space_id": spaceID,
}).Decode(&membership)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("membership not found")
}
return nil, err
}
return &membership, nil
}
// GetSpaceMembers retrieves all members in a space
func (r *MembershipRepository) GetSpaceMembers(ctx context.Context, spaceID bson.ObjectID) ([]*entities.Membership, error) {
var memberships []*entities.Membership
cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &memberships); err != nil {
return nil, err
}
return memberships, nil
}
// GetUserMemberships retrieves all memberships for a user
func (r *MembershipRepository) GetUserMemberships(ctx context.Context, userID bson.ObjectID) ([]*entities.Membership, error) {
var memberships []*entities.Membership
cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &memberships); err != nil {
return nil, err
}
return memberships, nil
}
// UpdateMembership updates a membership
func (r *MembershipRepository) UpdateMembership(ctx context.Context, membership *entities.Membership) error {
_, err := r.collection.ReplaceOne(ctx, bson.M{"_id": membership.ID}, membership)
return err
}
// DeleteMembership deletes a membership
func (r *MembershipRepository) DeleteMembership(ctx context.Context, id bson.ObjectID) error {
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
return err
}
// DeleteMembershipsBySpaceID deletes all memberships for a space
func (r *MembershipRepository) DeleteMembershipsBySpaceID(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 *MembershipRepository) EnsureIndexes(ctx context.Context) error {
indexModel := []mongo.IndexModel{
{
Keys: bson.D{bson.E{Key: "user_id", Value: 1}, bson.E{Key: "space_id", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{bson.E{Key: "space_id", Value: 1}},
},
}
_, err := r.collection.Indexes().CreateMany(ctx, indexModel)
return err
}

View File

@@ -0,0 +1,120 @@
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"
"github.com/noteapp/backend/internal/domain/entities"
)
// UserRepository implements the user repository interface
type UserRepository struct {
collection *mongo.Collection
}
// NewUserRepository creates a new user repository
func NewUserRepository(db *mongo.Database) *UserRepository {
return &UserRepository{
collection: db.Collection("users"),
}
}
// CreateUser creates a new user
func (r *UserRepository) CreateUser(ctx context.Context, user *entities.User) error {
user.ID = bson.NewObjectID()
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
_, err := r.collection.InsertOne(ctx, user)
return err
}
// GetUserByID retrieves a user by ID
func (r *UserRepository) GetUserByID(ctx context.Context, id bson.ObjectID) (*entities.User, error) {
var user entities.User
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
// GetUserByEmail retrieves a user by email
func (r *UserRepository) GetUserByEmail(ctx context.Context, email string) (*entities.User, error) {
var user entities.User
err := r.collection.FindOne(ctx, bson.M{"email": email}).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
// GetUserByUsername retrieves a user by username
func (r *UserRepository) GetUserByUsername(ctx context.Context, username string) (*entities.User, error) {
var user entities.User
err := r.collection.FindOne(ctx, bson.M{"username": username}).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
// UpdateUser updates a user
func (r *UserRepository) UpdateUser(ctx context.Context, user *entities.User) error {
user.UpdatedAt = time.Now()
_, err := r.collection.ReplaceOne(ctx, bson.M{"_id": user.ID}, user)
return err
}
// DeleteUser deletes a user
func (r *UserRepository) DeleteUser(ctx context.Context, id bson.ObjectID) error {
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
return err
}
// ListAllUsers retrieves all users sorted by creation date descending
func (r *UserRepository) ListAllUsers(ctx context.Context) ([]*entities.User, error) {
opts := options.Find().SetSort(bson.D{bson.E{Key: "created_at", Value: -1}})
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var users []*entities.User
if err := cursor.All(ctx, &users); err != nil {
return nil, err
}
return users, nil
}
// EnsureIndexes creates necessary indexes for users collection
func (r *UserRepository) EnsureIndexes(ctx context.Context) error {
indexModel := []mongo.IndexModel{
{
Keys: bson.D{bson.E{Key: "email", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{bson.E{Key: "username", Value: 1}},
Options: options.Index().SetUnique(true),
},
}
_, err := r.collection.Indexes().CreateMany(ctx, indexModel)
return err
}