216 lines
6.7 KiB
Go
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
|
|
}
|