package repositoryimp import ( "context" "errors" "fmt" "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder" rd "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/decoder" "git.sendico.io/tech/sxchnge/bff/pkg/db/storable" "git.sendico.io/tech/sxchnge/bff/pkg/db/transaction" "git.sendico.io/tech/sxchnge/bff/pkg/merrors" "git.sendico.io/tech/sxchnge/bff/pkg/model" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) type MongoRepository struct { collectionName string collection *mongo.Collection } func idFilter(id bson.ObjectID) bson.D { return bson.D{ {Key: storable.IDField, Value: id}, } } func NewMongoRepository(db *mongo.Database, collection string) *MongoRepository { return &MongoRepository{ collectionName: collection, collection: db.Collection(collection), } } func (r *MongoRepository) Collection() string { return r.collectionName } func prepareForWrite(obj storable.Storable) bson.ObjectID { id := obj.GetID() if id == nil || id.IsZero() { obj.SetID(bson.NewObjectID()) id = obj.GetID() } obj.Update() return *id } func (r *MongoRepository) Insert(ctx context.Context, obj storable.Storable, getFilter builder.Query) error { prepareForWrite(obj) _, err := r.collection.InsertOne(ctx, obj) if mongo.IsDuplicateKeyError(err) { if getFilter != nil { if err = r.FindOneByFilter(ctx, getFilter, obj); err != nil { return err } } return merrors.DataConflict("duplicate_key") } return err } func (r *MongoRepository) InsertMany(ctx context.Context, objects []storable.Storable) error { if len(objects) == 0 { return nil } docs := make([]interface{}, len(objects)) for i, obj := range objects { prepareForWrite(obj) docs[i] = obj } _, err := r.collection.InsertMany(ctx, docs) return err } func (r *MongoRepository) findOneByFilterImp(ctx context.Context, filter bson.D, errMessage string, result storable.Storable) error { err := r.collection.FindOne(ctx, filter).Decode(result) if errors.Is(err, mongo.ErrNoDocuments) { return merrors.NoData(errMessage) } return err } func (r *MongoRepository) Get(ctx context.Context, id bson.ObjectID, result storable.Storable) error { if id.IsZero() { return merrors.InvalidArgument("zero id provided while fetching "+result.Collection(), "id") } return r.findOneByFilterImp(ctx, idFilter(id), fmt.Sprintf("%s with ID = %s not found", result.Collection(), id.Hex()), result) } type QueryFunc func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error) func (r *MongoRepository) executeQuery(ctx context.Context, queryFunc QueryFunc, decoder rd.DecodingFunc) error { cursor, err := queryFunc(ctx, r.collection) if errors.Is(err, mongo.ErrNoDocuments) { return merrors.NoData("no_items_in_array") } if err != nil { return err } defer func() { _ = cursor.Close(ctx) }() for cursor.Next(ctx) { if err = decoder(cursor); err != nil { return err } } return nil } func (r *MongoRepository) Aggregate(ctx context.Context, pipeline builder.Pipeline, decoder rd.DecodingFunc) error { queryFunc := func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error) { return collection.Aggregate(ctx, pipeline.Build()) } return r.executeQuery(ctx, queryFunc, decoder) } func (r *MongoRepository) FindManyByFilter(ctx context.Context, query builder.Query, decoder rd.DecodingFunc) error { queryFunc := func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error) { return collection.Find(ctx, query.BuildQuery(), query.BuildOptions()) } return r.executeQuery(ctx, queryFunc, decoder) } func (r *MongoRepository) FindAllByFilter(ctx context.Context, query builder.Query, result any) error { if query == nil { cursor, err := r.collection.Find(ctx, bson.D{}) if err != nil { return err } defer func() { _ = cursor.Close(ctx) }() return cursor.All(ctx, result) } cursor, err := r.collection.Find(ctx, query.BuildQuery(), query.BuildOptions()) if err != nil { return err } defer func() { _ = cursor.Close(ctx) }() return cursor.All(ctx, result) } func (r *MongoRepository) FindOneByFilter(ctx context.Context, query builder.Query, result storable.Storable) error { return r.findOneByFilterImp(ctx, query.BuildQuery(), result.Collection()+" not found by filter", result) } func (r *MongoRepository) Update(ctx context.Context, obj storable.Storable) error { obj.Update() return r.collection.FindOneAndReplace(ctx, idFilter(*obj.GetID()), obj).Err() } func (r *MongoRepository) ReplaceOneByFilter(ctx context.Context, query builder.Query, obj storable.Storable) (bool, error) { if query == nil { return false, merrors.InvalidArgument("filter is required", "filter") } if obj == nil { return false, merrors.InvalidArgument("object is required", "obj") } result, err := r.collection.ReplaceOne(ctx, query.BuildQuery(), obj) if mongo.IsDuplicateKeyError(err) { return false, merrors.DataConflict("duplicate_key") } if err != nil { return false, err } return result.MatchedCount > 0, nil } func (r *MongoRepository) Upsert(ctx context.Context, obj storable.Storable) error { id := prepareForWrite(obj) _, err := r.collection.ReplaceOne(ctx, idFilter(id), obj, options.Replace().SetUpsert(true)) return err } func (r *MongoRepository) Patch(ctx context.Context, id bson.ObjectID, patch builder.Patch) error { if id.IsZero() { return merrors.InvalidArgument("zero id provided while patching", "id") } _, err := r.collection.UpdateByID(ctx, id, patch.Build()) return err } func (r *MongoRepository) PatchMany(ctx context.Context, query builder.Query, patch builder.Patch) (int, error) { result, err := r.collection.UpdateMany(ctx, query.BuildQuery(), patch.Build()) if err != nil { return 0, err } return int(result.ModifiedCount), nil } func (r *MongoRepository) ListIDs(ctx context.Context, query builder.Query) ([]bson.ObjectID, error) { filter := query.BuildQuery() findOptions := query.BuildOptions().SetProjection(bson.M{storable.IDField: 1}) cursor, err := r.collection.Find(ctx, filter, findOptions) if err != nil { return nil, err } defer func() { _ = cursor.Close(ctx) }() var ids []bson.ObjectID for cursor.Next(ctx) { var doc struct { ID bson.ObjectID `bson:"_id"` } if err := cursor.Decode(&doc); err != nil { return nil, err } ids = append(ids, doc.ID) } if err := cursor.Err(); err != nil { return nil, err } return ids, nil } func (r *MongoRepository) ListPermissionBound(ctx context.Context, query builder.Query) ([]model.PermissionBoundStorable, error) { return listProjectedStorables(ctx, r.collection, query, bson.M{ storable.IDField: 1, storable.PermissionRefField: 1, storable.OrganizationRefField: 1, }, func(doc *model.PermissionBound) model.PermissionBoundStorable { return doc }) } func (r *MongoRepository) ListAccountBound(ctx context.Context, query builder.Query) ([]model.AccountBoundStorable, error) { return listProjectedStorables(ctx, r.collection, query, bson.M{ storable.IDField: 1, model.AccountRefField: 1, model.OrganizationRefField: 1, }, func(doc *model.AccountBoundBase) model.AccountBoundStorable { return doc }) } func listProjectedStorables[T any, R any]( ctx context.Context, collection *mongo.Collection, query builder.Query, projection bson.M, wrap func(*T) R, ) ([]R, error) { filter := query.BuildQuery() cursor, err := collection.Find(ctx, filter, options.Find().SetProjection(projection)) if err != nil { return nil, err } defer func() { _ = cursor.Close(ctx) }() result := make([]R, 0) for cursor.Next(ctx) { doc := new(T) if err := cursor.Decode(doc); err != nil { return nil, err } result = append(result, wrap(doc)) } if err := cursor.Err(); err != nil { return nil, err } return result, nil } func (r *MongoRepository) Delete(ctx context.Context, id bson.ObjectID) error { _, err := r.collection.DeleteOne(ctx, idFilter(id)) return err } func (r *MongoRepository) DeleteMany(ctx context.Context, query builder.Query) error { _, err := r.collection.DeleteMany(ctx, query.BuildQuery()) return err } func (r *MongoRepository) Name() string { return r.collection.Name() } func (r *MongoRepository) WithTransaction(ctx context.Context, cb transaction.Callback) (any, error) { if cb == nil { return nil, merrors.InvalidArgument("transaction callback is required", "callback") } session, err := r.collection.Database().Client().StartSession() if err != nil { return nil, err } defer session.EndSession(ctx) return session.WithTransaction(ctx, cb) }