Files
spsays-bff/internal/repository/disciple_repo.go
T
2026-08-07 22:46:08 +05:30

216 lines
6.7 KiB
Go

package repository
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"
)
type DiscipleRepo struct {
col *mongo.Collection
}
func NewDiscipleRepo(db *MongoDB) *DiscipleRepo {
return &DiscipleRepo{col: db.Collection("disciples")}
}
type DiscipleDoc struct {
ID bson.ObjectID `bson:"_id" json:"id"`
InitiatedName string `bson:"initiatedName" json:"initiated_name"`
Suffix string `bson:"suffix" json:"suffix"`
InitiationDate *time.Time `bson:"initiationDate,omitempty" json:"initiation_date,omitempty"`
InitiationPlace *string `bson:"initiationPlace,omitempty" json:"initiation_place,omitempty"`
KarmiName *string `bson:"karmiName,omitempty" json:"karmi_name,omitempty"`
Avatar *string `bson:"avatar,omitempty" json:"avatar,omitempty"`
About *string `bson:"about,omitempty" json:"about,omitempty"`
Tags []string `bson:"tags" json:"tags"`
IsVerified bool `bson:"isVerified" json:"is_verified"`
}
type DiscipleSummaryDoc struct {
ID bson.ObjectID `bson:"_id" json:"id"`
InitiatedName string `bson:"initiatedName" json:"initiated_name"`
Suffix string `bson:"suffix" json:"suffix"`
InitiationDate *time.Time `bson:"initiationDate,omitempty" json:"initiation_date,omitempty"`
InitiationPlace *string `bson:"initiationPlace,omitempty" json:"initiation_place,omitempty"`
IsVerified bool `bson:"isVerified" json:"is_verified"`
}
var discipleSummaryProjection = bson.M{
"initiatedName": 1, "suffix": 1, "initiationDate": 1,
"initiationPlace": 1, "isVerified": 1,
}
func (r *DiscipleRepo) GetByLetter(ctx context.Context, letter string) ([]DiscipleSummaryDoc, error) {
// Case-insensitive starts-with using regex
filter := bson.M{"initiatedName": bson.M{"$regex": "^" + letter, "$options": "i"}}
opts := options.Find().
SetSort(bson.M{"initiatedName": 1}).
SetProjection(discipleSummaryProjection)
cursor, err := r.col.Find(ctx, filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []DiscipleSummaryDoc
if err := cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}
func (r *DiscipleRepo) GetByLetterAndSuffix(ctx context.Context, letter *string, suffix *string) ([]DiscipleSummaryDoc, error) {
filter := bson.M{}
if letter != nil {
filter["initiatedName"] = bson.M{"$regex": "^" + *letter, "$options": "i"}
}
if suffix != nil && *suffix != "all" {
filter["suffix"] = *suffix
}
opts := options.Find().
SetSort(bson.M{"initiatedName": 1}).
SetProjection(discipleSummaryProjection)
cursor, err := r.col.Find(ctx, filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []DiscipleSummaryDoc
if err := cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}
// FindByIDs returns summary docs for the given ObjectID hex strings (order
// unspecified). Ids that aren't valid ObjectIDs are skipped rather than
// failing the batch — the ids come from a Postgres column with no FK, so a
// malformed or stale value must degrade gracefully. An empty/all-invalid
// input short-circuits without hitting Mongo.
//
// Used to hydrate disciple names onto admin user rows: the user→disciple link
// lives in Postgres while the disciple record lives here.
func (r *DiscipleRepo) FindByIDs(ctx context.Context, ids []string) ([]DiscipleSummaryDoc, error) {
oids := make([]bson.ObjectID, 0, len(ids))
for _, id := range ids {
if oid, err := bson.ObjectIDFromHex(id); err == nil {
oids = append(oids, oid)
}
}
if len(oids) == 0 {
return []DiscipleSummaryDoc{}, nil
}
cursor, err := r.col.Find(ctx,
bson.M{"_id": bson.M{"$in": oids}},
options.Find().SetProjection(discipleSummaryProjection),
)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []DiscipleSummaryDoc
if err := cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}
// FindByID returns one disciple summary, or nil when the id is malformed or
// no such document exists. Callers use it to validate a link target before
// writing it to Postgres.
func (r *DiscipleRepo) FindByID(ctx context.Context, id string) (*DiscipleSummaryDoc, error) {
oid, err := bson.ObjectIDFromHex(id)
if err != nil {
return nil, nil // malformed id — "not found", not an error
}
var doc DiscipleSummaryDoc
err = r.col.FindOne(ctx, bson.M{"_id": oid},
options.FindOne().SetProjection(discipleSummaryProjection),
).Decode(&doc)
if errors.Is(err, mongo.ErrNoDocuments) {
return nil, nil
}
if err != nil {
return nil, err
}
return &doc, nil
}
func (r *DiscipleRepo) Count(ctx context.Context) (int64, error) {
return r.col.CountDocuments(ctx, bson.M{})
}
func (r *DiscipleRepo) CountFiltered(ctx context.Context, letter *string, suffix *string) (int64, error) {
filter := bson.M{}
if letter != nil {
filter["initiatedName"] = bson.M{"$regex": "^" + *letter, "$options": "i"}
}
if suffix != nil && *suffix != "all" {
filter["suffix"] = *suffix
}
return r.col.CountDocuments(ctx, filter)
}
func (r *DiscipleRepo) GetLetterCounts(ctx context.Context) ([]bson.M, error) {
pipeline := bson.A{
bson.M{"$group": bson.M{
"_id": bson.M{"$toUpper": bson.M{"$substrBytes": bson.A{"$initiatedName", 0, 1}}},
"count": bson.M{"$sum": 1},
}},
bson.M{"$sort": bson.M{"_id": 1}},
}
cursor, err := r.col.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []bson.M
if err := cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}
func (r *DiscipleRepo) GetSuffixCounts(ctx context.Context) ([]bson.M, error) {
pipeline := bson.A{
bson.M{"$group": bson.M{"_id": "$suffix", "count": bson.M{"$sum": 1}}},
bson.M{"$sort": bson.M{"count": -1}},
}
cursor, err := r.col.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []bson.M
if err := cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}
func (r *DiscipleRepo) Search(ctx context.Context, query string, limit int) ([]DiscipleSummaryDoc, error) {
filter := bson.M{"initiatedName": bson.M{"$regex": query, "$options": "i"}}
opts := options.Find().
SetSort(bson.M{"initiatedName": 1}).
SetLimit(int64(limit)).
SetProjection(discipleSummaryProjection)
cursor, err := r.col.Find(ctx, filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []DiscipleSummaryDoc
if err := cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}