feat: add books APIs
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type BookRepo struct {
|
||||
meta *mongo.Collection
|
||||
verses *mongo.Collection
|
||||
proseChapters *mongo.Collection
|
||||
}
|
||||
|
||||
func NewBookRepo(db *MongoDB) *BookRepo {
|
||||
return &BookRepo{
|
||||
meta: db.Collection("book_meta"),
|
||||
verses: db.Collection("verses"),
|
||||
proseChapters: db.Collection("prose_chapters"),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Book Meta ---
|
||||
|
||||
type BookMetaDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Data interface{} `bson:"data" json:"data"`
|
||||
}
|
||||
|
||||
func (r *BookRepo) GetAllBookMetas(ctx context.Context) ([]BookMetaDoc, error) {
|
||||
cursor, err := r.meta.Find(ctx, bson.M{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []BookMetaDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *BookRepo) GetBookMeta(ctx context.Context, book string) (*BookMetaDoc, error) {
|
||||
var doc BookMetaDoc
|
||||
err := r.meta.FindOne(ctx, bson.M{"book": book}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// --- Verses ---
|
||||
|
||||
type SynonymDoc struct {
|
||||
Word string `bson:"word" json:"word"`
|
||||
Meanings []string `bson:"meanings" json:"meanings"`
|
||||
}
|
||||
|
||||
type VerseDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Division *string `bson:"division,omitempty" json:"division,omitempty"`
|
||||
Chapter int `bson:"chapter" json:"chapter"`
|
||||
Verse int `bson:"verse" json:"verse"`
|
||||
VerseEnd *int `bson:"verseEnd,omitempty" json:"verse_end,omitempty"`
|
||||
Reference string `bson:"reference" json:"reference"`
|
||||
BookTitle string `bson:"bookTitle" json:"book_title"`
|
||||
Devanagari string `bson:"devanagari" json:"devanagari"`
|
||||
Bengali *string `bson:"bengali,omitempty" json:"bengali,omitempty"`
|
||||
Transliteration string `bson:"transliteration" json:"transliteration"`
|
||||
Synonyms []SynonymDoc `bson:"synonyms" json:"synonyms"`
|
||||
Translation string `bson:"translation" json:"translation"`
|
||||
Purport []string `bson:"purport" json:"purport"`
|
||||
}
|
||||
|
||||
type VerseIndexDoc struct {
|
||||
Verse int `bson:"verse" json:"verse"`
|
||||
VerseEnd *int `bson:"verseEnd,omitempty" json:"verse_end,omitempty"`
|
||||
Reference string `bson:"reference" json:"reference"`
|
||||
}
|
||||
|
||||
func (r *BookRepo) GetVerseIndex(ctx context.Context, book string, division *string, chapter int) ([]VerseIndexDoc, error) {
|
||||
filter := bson.M{"book": book, "chapter": chapter}
|
||||
if division != nil {
|
||||
filter["division"] = *division
|
||||
} else {
|
||||
filter["division"] = nil
|
||||
}
|
||||
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"verse": 1}).
|
||||
SetProjection(bson.M{"_id": 0, "verse": 1, "verseEnd": 1, "reference": 1})
|
||||
|
||||
cursor, err := r.verses.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []VerseIndexDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *BookRepo) GetVerses(ctx context.Context, book string, division *string, chapter int, page, limit int) ([]VerseDoc, int64, error) {
|
||||
filter := bson.M{"book": book, "chapter": chapter}
|
||||
if division != nil {
|
||||
filter["division"] = *division
|
||||
} else {
|
||||
filter["division"] = nil
|
||||
}
|
||||
|
||||
total, err := r.verses.CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
skip := int64((page - 1) * limit)
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"verse": 1}).
|
||||
SetSkip(skip).
|
||||
SetLimit(int64(limit))
|
||||
|
||||
cursor, err := r.verses.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []VerseDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return results, total, nil
|
||||
}
|
||||
|
||||
func (r *BookRepo) GetSingleVerse(ctx context.Context, book string, division *string, chapter, verse int) (*VerseDoc, error) {
|
||||
filter := bson.M{"book": book, "chapter": chapter, "verse": verse}
|
||||
if division != nil {
|
||||
filter["division"] = *division
|
||||
} else {
|
||||
filter["division"] = nil
|
||||
}
|
||||
|
||||
var doc VerseDoc
|
||||
err := r.verses.FindOne(ctx, filter).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// --- Prose Chapters ---
|
||||
|
||||
type ProseChapterDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Volume int `bson:"volume" json:"volume"`
|
||||
Chapter *int `bson:"chapter,omitempty" json:"chapter,omitempty"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Type string `bson:"type" json:"type"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Order int `bson:"order" json:"order"`
|
||||
Paragraphs []string `bson:"paragraphs" json:"paragraphs"`
|
||||
}
|
||||
|
||||
func (r *BookRepo) GetProseChapters(ctx context.Context, book string, volume int, chapterType *string) ([]ProseChapterDoc, error) {
|
||||
filter := bson.M{"book": book, "volume": volume}
|
||||
if chapterType != nil {
|
||||
filter["type"] = *chapterType
|
||||
}
|
||||
|
||||
opts := options.Find().SetSort(bson.M{"order": 1})
|
||||
cursor, err := r.proseChapters.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []ProseChapterDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *BookRepo) GetProseChapter(ctx context.Context, book string, volume int, slug string) (*ProseChapterDoc, error) {
|
||||
filter := bson.M{"book": book, "volume": volume, "slug": slug}
|
||||
var doc ProseChapterDoc
|
||||
err := r.proseChapters.FindOne(ctx, filter).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type EssayRepo struct {
|
||||
col *mongo.Collection
|
||||
}
|
||||
|
||||
func NewEssayRepo(db *MongoDB) *EssayRepo {
|
||||
return &EssayRepo{col: db.Collection("essays")}
|
||||
}
|
||||
|
||||
type EssayDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Author string `bson:"author" json:"author"`
|
||||
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
||||
Group *string `bson:"group,omitempty" json:"group,omitempty"`
|
||||
Order int `bson:"order" json:"order"`
|
||||
Paragraphs []string `bson:"paragraphs" json:"paragraphs"`
|
||||
}
|
||||
|
||||
type EssaySummaryDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
||||
Group *string `bson:"group,omitempty" json:"group,omitempty"`
|
||||
}
|
||||
|
||||
func (r *EssayRepo) GetAll(ctx context.Context) ([]EssaySummaryDoc, error) {
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"group": 1, "order": 1}).
|
||||
SetProjection(bson.M{"slug": 1, "title": 1, "year": 1, "group": 1})
|
||||
cursor, err := r.col.Find(ctx, bson.M{}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []EssaySummaryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *EssayRepo) GetBySlug(ctx context.Context, slug string) (*EssayDoc, error) {
|
||||
var doc EssayDoc
|
||||
err := r.col.FindOne(ctx, bson.M{"slug": slug}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type GlossaryRepo struct {
|
||||
col *mongo.Collection
|
||||
}
|
||||
|
||||
func NewGlossaryRepo(db *MongoDB) *GlossaryRepo {
|
||||
return &GlossaryRepo{col: db.Collection("glossary")}
|
||||
}
|
||||
|
||||
// GlossaryEntryDoc is the full glossary entry with definition.
|
||||
type GlossaryEntryDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Term string `bson:"term" json:"term"`
|
||||
Definition string `bson:"definition" json:"definition"`
|
||||
DefinitionHTML string `bson:"definitionHtml" json:"definition_html"`
|
||||
}
|
||||
|
||||
// GlossaryTermSummary is a lightweight entry for sidebar listing (no definition).
|
||||
type GlossaryTermSummary struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Term string `bson:"term" json:"term"`
|
||||
}
|
||||
|
||||
// GlossaryLetterCount holds the count of entries per base letter.
|
||||
type GlossaryLetterCount struct {
|
||||
Letter string `json:"letter"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// diacriticToBase maps diacritical first characters to ASCII base letters.
|
||||
// Must match the frontend mapping in the glossary page.
|
||||
var diacriticToBase = map[rune]rune{
|
||||
'Ā': 'A', 'Ä': 'A',
|
||||
'Ī': 'I', 'É': 'I',
|
||||
'Ū': 'U', 'Ü': 'U',
|
||||
'Ṛ': 'R', 'Ṝ': 'R',
|
||||
'Ś': 'S', 'Ç': 'S', 'Ṣ': 'S',
|
||||
'Ñ': 'N', 'Ṇ': 'N', 'Ṅ': 'N',
|
||||
'Ṭ': 'T',
|
||||
'Ḍ': 'D',
|
||||
'Ḥ': 'H',
|
||||
// Lowercase equivalents
|
||||
'ā': 'A', 'ä': 'A',
|
||||
'ī': 'I', 'é': 'I',
|
||||
'ū': 'U', 'ü': 'U',
|
||||
'ṛ': 'R', 'ṝ': 'R',
|
||||
'ś': 'S', 'ç': 'S', 'ṣ': 'S',
|
||||
'ñ': 'N', 'ṇ': 'N', 'ṅ': 'N',
|
||||
'ṭ': 'T',
|
||||
'ḍ': 'D',
|
||||
'ḥ': 'H',
|
||||
}
|
||||
|
||||
// baseLetter returns the ASCII base letter for the first character of a term.
|
||||
func baseLetter(term string) string {
|
||||
if term == "" {
|
||||
return "?"
|
||||
}
|
||||
first := []rune(term)[0]
|
||||
if base, ok := diacriticToBase[first]; ok {
|
||||
return string(base)
|
||||
}
|
||||
return strings.ToUpper(string(first))
|
||||
}
|
||||
|
||||
// regexPrefixes returns the set of first characters (including diacritical variants)
|
||||
// that map to the given base letter, for use in a MongoDB $regex query.
|
||||
func regexPrefixes(letter string) string {
|
||||
upper := strings.ToUpper(letter)
|
||||
if len(upper) == 0 {
|
||||
return ""
|
||||
}
|
||||
base := rune(upper[0])
|
||||
|
||||
// Collect all characters that map to this base letter
|
||||
chars := []rune{base, rune(strings.ToLower(upper)[0])}
|
||||
for k, v := range diacriticToBase {
|
||||
if v == base {
|
||||
chars = append(chars, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Build regex character class: ^[AĀÄāä]
|
||||
return "^[" + string(chars) + "]"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GetAll returns all glossary entries sorted by term.
|
||||
func (r *GlossaryRepo) GetAll(ctx context.Context) ([]GlossaryEntryDoc, error) {
|
||||
opts := options.Find().SetSort(bson.M{"term": 1})
|
||||
cursor, err := r.col.Find(ctx, bson.M{}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []GlossaryEntryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetByTerm returns a single glossary entry by exact term match.
|
||||
func (r *GlossaryRepo) GetByTerm(ctx context.Context, term string) (*GlossaryEntryDoc, error) {
|
||||
var doc GlossaryEntryDoc
|
||||
err := r.col.FindOne(ctx, bson.M{"term": term}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// GetByLetter returns all glossary entries whose term starts with the given
|
||||
// base letter (including diacritical variants). Sorted by term.
|
||||
func (r *GlossaryRepo) GetByLetter(ctx context.Context, letter string) ([]GlossaryEntryDoc, error) {
|
||||
regex := regexPrefixes(letter)
|
||||
if regex == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
filter := bson.M{"term": bson.M{"$regex": regex}}
|
||||
opts := options.Find().SetSort(bson.M{"term": 1})
|
||||
|
||||
cursor, err := r.col.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []GlossaryEntryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetTermsByLetter returns lightweight term summaries (no definitions) for a
|
||||
// given base letter. Used for sidebar listing.
|
||||
func (r *GlossaryRepo) GetTermsByLetter(ctx context.Context, letter string) ([]GlossaryTermSummary, error) {
|
||||
regex := regexPrefixes(letter)
|
||||
if regex == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
filter := bson.M{"term": bson.M{"$regex": regex}}
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"term": 1}).
|
||||
SetProjection(bson.M{"term": 1})
|
||||
|
||||
cursor, err := r.col.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var results []GlossaryTermSummary
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetLetterCounts returns the count of entries per base letter.
|
||||
// This is computed in Go since MongoDB can't easily map diacritical first
|
||||
// characters to base letters in an aggregation pipeline.
|
||||
func (r *GlossaryRepo) GetLetterCounts(ctx context.Context) ([]GlossaryLetterCount, error) {
|
||||
// Fetch only the term field
|
||||
opts := options.Find().SetProjection(bson.M{"term": 1})
|
||||
cursor, err := r.col.Find(ctx, bson.M{}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var docs []struct {
|
||||
Term string `bson:"term"`
|
||||
}
|
||||
if err := cursor.All(ctx, &docs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts := make(map[string]int)
|
||||
for _, doc := range docs {
|
||||
letter := baseLetter(doc.Term)
|
||||
counts[letter]++
|
||||
}
|
||||
|
||||
// Convert to sorted slice
|
||||
result := make([]GlossaryLetterCount, 0, len(counts))
|
||||
for letter, count := range counts {
|
||||
result = append(result, GlossaryLetterCount{Letter: letter, Count: count})
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
for i := 0; i < len(result); i++ {
|
||||
for j := i + 1; j < len(result); j++ {
|
||||
if result[j].Letter < result[i].Letter {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type JournalRepo struct {
|
||||
meta *mongo.Collection
|
||||
chapters *mongo.Collection
|
||||
}
|
||||
|
||||
func NewJournalRepo(db *MongoDB) *JournalRepo {
|
||||
return &JournalRepo{
|
||||
meta: db.Collection("journal_meta"),
|
||||
chapters: db.Collection("journal_chapters"),
|
||||
}
|
||||
}
|
||||
|
||||
type JournalMetaDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Data interface{} `bson:"data" json:"data"`
|
||||
}
|
||||
|
||||
type JournalChapterDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Chapter int `bson:"chapter" json:"chapter"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Type string `bson:"type" json:"type"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Order int `bson:"order" json:"order"`
|
||||
Paragraphs []string `bson:"paragraphs" json:"paragraphs"`
|
||||
}
|
||||
|
||||
func (r *JournalRepo) GetAllMetas(ctx context.Context) ([]JournalMetaDoc, error) {
|
||||
cursor, err := r.meta.Find(ctx, bson.M{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []JournalMetaDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *JournalRepo) GetMeta(ctx context.Context, book string) (*JournalMetaDoc, error) {
|
||||
var doc JournalMetaDoc
|
||||
err := r.meta.FindOne(ctx, bson.M{"book": book}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func (r *JournalRepo) GetChapters(ctx context.Context, book string) ([]JournalChapterDoc, error) {
|
||||
opts := options.Find().SetSort(bson.M{"order": 1})
|
||||
cursor, err := r.chapters.Find(ctx, bson.M{"book": book}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []JournalChapterDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *JournalRepo) GetChapter(ctx context.Context, book, slug string) (*JournalChapterDoc, error) {
|
||||
var doc JournalChapterDoc
|
||||
err := r.chapters.FindOne(ctx, bson.M{"book": book, "slug": slug}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type LetterRepo struct {
|
||||
col *mongo.Collection
|
||||
}
|
||||
|
||||
func NewLetterRepo(db *MongoDB) *LetterRepo {
|
||||
return &LetterRepo{col: db.Collection("letters")}
|
||||
}
|
||||
|
||||
type LetterDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Recipients []string `bson:"recipients" json:"recipients"`
|
||||
RecipientDisplay string `bson:"recipientDisplay" json:"recipient_display"`
|
||||
Date *string `bson:"date,omitempty" json:"date,omitempty"`
|
||||
DateRef *string `bson:"dateRef,omitempty" json:"date_ref,omitempty"`
|
||||
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
||||
Month *int `bson:"month,omitempty" json:"month,omitempty"`
|
||||
Day *int `bson:"day,omitempty" json:"day,omitempty"`
|
||||
Location *string `bson:"location,omitempty" json:"location,omitempty"`
|
||||
Salutation *string `bson:"salutation,omitempty" json:"salutation,omitempty"`
|
||||
Paragraphs []string `bson:"paragraphs" json:"paragraphs"`
|
||||
}
|
||||
|
||||
type LetterSummaryDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Recipients []string `bson:"recipients" json:"recipients"`
|
||||
RecipientDisplay string `bson:"recipientDisplay" json:"recipient_display"`
|
||||
Date *string `bson:"date,omitempty" json:"date,omitempty"`
|
||||
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
||||
Month *int `bson:"month,omitempty" json:"month,omitempty"`
|
||||
Location *string `bson:"location,omitempty" json:"location,omitempty"`
|
||||
}
|
||||
|
||||
var letterSummaryProjection = bson.M{
|
||||
"slug": 1, "recipients": 1, "recipientDisplay": 1,
|
||||
"date": 1, "year": 1, "month": 1, "location": 1,
|
||||
}
|
||||
|
||||
func (r *LetterRepo) GetBySlug(ctx context.Context, slug string) (*LetterDoc, error) {
|
||||
var doc LetterDoc
|
||||
err := r.col.FindOne(ctx, bson.M{"slug": slug}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func (r *LetterRepo) GetByYear(ctx context.Context, year int) ([]LetterSummaryDoc, error) {
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"month": 1, "day": 1}).
|
||||
SetProjection(letterSummaryProjection)
|
||||
cursor, err := r.col.Find(ctx, bson.M{"year": year}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []LetterSummaryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *LetterRepo) GetByRecipient(ctx context.Context, name string) ([]LetterSummaryDoc, error) {
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"year": 1, "month": 1, "day": 1}).
|
||||
SetProjection(letterSummaryProjection)
|
||||
cursor, err := r.col.Find(ctx, bson.M{"recipients": name}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []LetterSummaryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *LetterRepo) GetByLocation(ctx context.Context, location string) ([]LetterSummaryDoc, error) {
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"year": 1, "month": 1, "day": 1}).
|
||||
SetProjection(letterSummaryProjection)
|
||||
cursor, err := r.col.Find(ctx, bson.M{"location": location}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []LetterSummaryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *LetterRepo) GetYears(ctx context.Context) ([]bson.M, error) {
|
||||
pipeline := bson.A{
|
||||
bson.M{"$match": bson.M{"year": bson.M{"$ne": nil}}},
|
||||
bson.M{"$group": bson.M{"_id": "$year", "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 *LetterRepo) GetRecipients(ctx context.Context) ([]bson.M, error) {
|
||||
pipeline := bson.A{
|
||||
bson.M{"$unwind": "$recipients"},
|
||||
bson.M{"$group": bson.M{"_id": "$recipients", "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 *LetterRepo) GetLocations(ctx context.Context) ([]bson.M, error) {
|
||||
pipeline := bson.A{
|
||||
bson.M{"$match": bson.M{"location": bson.M{"$ne": nil}}},
|
||||
bson.M{"$group": bson.M{"_id": "$location", "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 *LetterRepo) Count(ctx context.Context) (int64, error) {
|
||||
return r.col.CountDocuments(ctx, bson.M{})
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type MagazineRepo struct {
|
||||
meta *mongo.Collection
|
||||
issues *mongo.Collection
|
||||
}
|
||||
|
||||
func NewMagazineRepo(db *MongoDB) *MagazineRepo {
|
||||
return &MagazineRepo{
|
||||
meta: db.Collection("magazine_meta"),
|
||||
issues: db.Collection("magazine_issues"),
|
||||
}
|
||||
}
|
||||
|
||||
type MagazineMetaDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Data interface{} `bson:"data" json:"data"`
|
||||
}
|
||||
|
||||
type MagazineIssueDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Volume *int `bson:"volume,omitempty" json:"volume,omitempty"`
|
||||
Part int `bson:"part" json:"part"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Order int `bson:"order" json:"order"`
|
||||
Paragraphs []string `bson:"paragraphs" json:"paragraphs"`
|
||||
}
|
||||
|
||||
func (r *MagazineRepo) GetAllMetas(ctx context.Context) ([]MagazineMetaDoc, error) {
|
||||
cursor, err := r.meta.Find(ctx, bson.M{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []MagazineMetaDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *MagazineRepo) GetMeta(ctx context.Context, book string) (*MagazineMetaDoc, error) {
|
||||
var doc MagazineMetaDoc
|
||||
err := r.meta.FindOne(ctx, bson.M{"book": book}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func (r *MagazineRepo) GetIssues(ctx context.Context, book string) ([]MagazineIssueDoc, error) {
|
||||
opts := options.Find().SetSort(bson.M{"order": 1})
|
||||
cursor, err := r.issues.Find(ctx, bson.M{"book": book}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []MagazineIssueDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *MagazineRepo) GetIssue(ctx context.Context, book, slug string) (*MagazineIssueDoc, error) {
|
||||
var doc MagazineIssueDoc
|
||||
err := r.issues.FindOne(ctx, bson.M{"book": book, "slug": slug}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// MongoDB holds the MongoDB client and database reference.
|
||||
type MongoDB struct {
|
||||
Client *mongo.Client
|
||||
DB *mongo.Database
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewMongoDB connects to MongoDB and returns a MongoDB instance.
|
||||
func NewMongoDB(uri, dbName string, logger *zap.Logger) (*MongoDB, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
clientOpts := options.Client().ApplyURI(uri)
|
||||
client, err := mongo.Connect(clientOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mongo connect: %w", err)
|
||||
}
|
||||
|
||||
if err := client.Ping(ctx, nil); err != nil {
|
||||
return nil, fmt.Errorf("mongo ping: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("connected to MongoDB", zap.String("db", dbName))
|
||||
|
||||
return &MongoDB{
|
||||
Client: client,
|
||||
DB: client.Database(dbName),
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close disconnects from MongoDB.
|
||||
func (m *MongoDB) Close() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return m.Client.Disconnect(ctx)
|
||||
}
|
||||
|
||||
// Collection returns a MongoDB collection handle.
|
||||
func (m *MongoDB) Collection(name string) *mongo.Collection {
|
||||
return m.DB.Collection(name)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type OtherDocumentRepo struct {
|
||||
meta *mongo.Collection
|
||||
chapters *mongo.Collection
|
||||
}
|
||||
|
||||
func NewOtherDocumentRepo(db *MongoDB) *OtherDocumentRepo {
|
||||
return &OtherDocumentRepo{
|
||||
meta: db.Collection("other_document_meta"),
|
||||
chapters: db.Collection("other_document_chapters"),
|
||||
}
|
||||
}
|
||||
|
||||
type OtherDocumentMetaDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Data interface{} `bson:"data" json:"data"`
|
||||
}
|
||||
|
||||
type OtherDocumentChapterDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Book string `bson:"book" json:"book"`
|
||||
Chapter int `bson:"chapter" json:"chapter"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Type string `bson:"type" json:"type"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Order int `bson:"order" json:"order"`
|
||||
Paragraphs []string `bson:"paragraphs" json:"paragraphs"`
|
||||
}
|
||||
|
||||
func (r *OtherDocumentRepo) GetAllMetas(ctx context.Context) ([]OtherDocumentMetaDoc, error) {
|
||||
cursor, err := r.meta.Find(ctx, bson.M{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []OtherDocumentMetaDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *OtherDocumentRepo) GetMeta(ctx context.Context, book string) (*OtherDocumentMetaDoc, error) {
|
||||
var doc OtherDocumentMetaDoc
|
||||
err := r.meta.FindOne(ctx, bson.M{"book": book}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func (r *OtherDocumentRepo) GetChapters(ctx context.Context, book string) ([]OtherDocumentChapterDoc, error) {
|
||||
opts := options.Find().SetSort(bson.M{"order": 1})
|
||||
cursor, err := r.chapters.Find(ctx, bson.M{"book": book}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []OtherDocumentChapterDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *OtherDocumentRepo) GetChapter(ctx context.Context, book, slug string) (*OtherDocumentChapterDoc, error) {
|
||||
var doc OtherDocumentChapterDoc
|
||||
err := r.chapters.FindOne(ctx, bson.M{"book": book, "slug": slug}).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type TranscriptionRepo struct {
|
||||
col *mongo.Collection
|
||||
}
|
||||
|
||||
func NewTranscriptionRepo(db *MongoDB) *TranscriptionRepo {
|
||||
return &TranscriptionRepo{col: db.Collection("transcriptions")}
|
||||
}
|
||||
|
||||
type ContentBlockDoc struct {
|
||||
Text string `bson:"text" json:"text"`
|
||||
Format string `bson:"format" json:"format"`
|
||||
Alignment string `bson:"alignment" json:"alignment"`
|
||||
Speaker *string `bson:"speaker,omitempty" json:"speaker,omitempty"`
|
||||
}
|
||||
|
||||
type TranscriptionDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Type string `bson:"type" json:"type"`
|
||||
Date *string `bson:"date,omitempty" json:"date,omitempty"`
|
||||
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
||||
Month *int `bson:"month,omitempty" json:"month,omitempty"`
|
||||
Day *int `bson:"day,omitempty" json:"day,omitempty"`
|
||||
Place *string `bson:"place,omitempty" json:"place,omitempty"`
|
||||
Context *string `bson:"context,omitempty" json:"context,omitempty"`
|
||||
Reference *string `bson:"reference,omitempty" json:"reference,omitempty"`
|
||||
DurationMinutes *int `bson:"durationMinutes,omitempty" json:"duration_minutes,omitempty"`
|
||||
AudioUrl *string `bson:"audioUrl,omitempty" json:"audio_url,omitempty"`
|
||||
Participants []string `bson:"participants" json:"participants"`
|
||||
Content []ContentBlockDoc `bson:"content" json:"content"`
|
||||
ContentJson *string `bson:"contentJson,omitempty" json:"-"`
|
||||
Tags []string `bson:"tags" json:"tags"`
|
||||
Language string `bson:"language" json:"language"`
|
||||
Source *string `bson:"source,omitempty" json:"source,omitempty"`
|
||||
IsPublished bool `bson:"isPublished" json:"is_published"`
|
||||
}
|
||||
|
||||
type TranscriptionSummaryDoc struct {
|
||||
ID bson.ObjectID `bson:"_id" json:"id"`
|
||||
Slug string `bson:"slug" json:"slug"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Type string `bson:"type" json:"type"`
|
||||
Date *string `bson:"date,omitempty" json:"date,omitempty"`
|
||||
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
||||
Month *int `bson:"month,omitempty" json:"month,omitempty"`
|
||||
Day *int `bson:"day,omitempty" json:"day,omitempty"`
|
||||
Place *string `bson:"place,omitempty" json:"place,omitempty"`
|
||||
Context *string `bson:"context,omitempty" json:"context,omitempty"`
|
||||
Reference *string `bson:"reference,omitempty" json:"reference,omitempty"`
|
||||
DurationMinutes *int `bson:"durationMinutes,omitempty" json:"duration_minutes,omitempty"`
|
||||
AudioUrl *string `bson:"audioUrl,omitempty" json:"audio_url,omitempty"`
|
||||
Participants []string `bson:"participants" json:"participants"`
|
||||
}
|
||||
|
||||
var publishedFilter = bson.M{"isPublished": true}
|
||||
|
||||
var transcriptionSummaryProjection = bson.M{
|
||||
"slug": 1, "name": 1, "type": 1, "date": 1,
|
||||
"year": 1, "month": 1, "day": 1, "place": 1,
|
||||
"context": 1, "reference": 1, "durationMinutes": 1,
|
||||
"audioUrl": 1, "participants": 1,
|
||||
}
|
||||
|
||||
func (r *TranscriptionRepo) GetBySlug(ctx context.Context, slug string) (*TranscriptionDoc, error) {
|
||||
filter := bson.M{"slug": slug, "isPublished": true}
|
||||
var doc TranscriptionDoc
|
||||
err := r.col.FindOne(ctx, filter).Decode(&doc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(doc.Content) == 0 && doc.ContentJson != nil && *doc.ContentJson != "" {
|
||||
var blocks []ContentBlockDoc
|
||||
if err := json.Unmarshal([]byte(*doc.ContentJson), &blocks); err == nil {
|
||||
doc.Content = blocks
|
||||
}
|
||||
}
|
||||
doc.ContentJson = nil
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func (r *TranscriptionRepo) GetByYear(ctx context.Context, year int) ([]TranscriptionSummaryDoc, error) {
|
||||
filter := bson.M{"year": year, "isPublished": true}
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"month": 1, "day": 1}).
|
||||
SetProjection(transcriptionSummaryProjection)
|
||||
cursor, err := r.col.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []TranscriptionSummaryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *TranscriptionRepo) GetByPlace(ctx context.Context, place string) ([]TranscriptionSummaryDoc, error) {
|
||||
filter := bson.M{"place": place, "isPublished": true}
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"year": 1, "month": 1, "day": 1}).
|
||||
SetProjection(transcriptionSummaryProjection)
|
||||
cursor, err := r.col.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []TranscriptionSummaryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *TranscriptionRepo) GetByType(ctx context.Context, docType string) ([]TranscriptionSummaryDoc, error) {
|
||||
filter := bson.M{"type": docType, "isPublished": true}
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"year": 1, "month": 1, "day": 1}).
|
||||
SetProjection(transcriptionSummaryProjection)
|
||||
cursor, err := r.col.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var results []TranscriptionSummaryDoc
|
||||
if err := cursor.All(ctx, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *TranscriptionRepo) GetYears(ctx context.Context) ([]bson.M, error) {
|
||||
pipeline := bson.A{
|
||||
bson.M{"$match": bson.M{"isPublished": true, "year": bson.M{"$ne": nil}}},
|
||||
bson.M{"$group": bson.M{"_id": "$year", "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 *TranscriptionRepo) GetPlaces(ctx context.Context) ([]bson.M, error) {
|
||||
pipeline := bson.A{
|
||||
bson.M{"$match": bson.M{"isPublished": true, "place": bson.M{"$ne": nil}}},
|
||||
bson.M{"$group": bson.M{"_id": "$place", "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 *TranscriptionRepo) GetTypeDistribution(ctx context.Context) (int64, int64, error) {
|
||||
lectureCount, err := r.col.CountDocuments(ctx, bson.M{"type": "lecture", "isPublished": true})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
conversationCount, err := r.col.CountDocuments(ctx, bson.M{"type": "conversation", "isPublished": true})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return lectureCount, conversationCount, nil
|
||||
}
|
||||
|
||||
func (r *TranscriptionRepo) Count(ctx context.Context) (int64, error) {
|
||||
return r.col.CountDocuments(ctx, publishedFilter)
|
||||
}
|
||||
|
||||
func (r *TranscriptionRepo) GetAdjacent(ctx context.Context, slug string, year int) (*TranscriptionSummaryDoc, *TranscriptionSummaryDoc, error) {
|
||||
filter := bson.M{"year": year, "isPublished": true}
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"month": 1, "day": 1}).
|
||||
SetProjection(transcriptionSummaryProjection)
|
||||
cursor, err := r.col.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var summaries []TranscriptionSummaryDoc
|
||||
if err := cursor.All(ctx, &summaries); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
idx := -1
|
||||
for i, s := range summaries {
|
||||
if s.Slug == slug {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var prev, next *TranscriptionSummaryDoc
|
||||
if idx > 0 {
|
||||
prev = &summaries[idx-1]
|
||||
}
|
||||
if idx < len(summaries)-1 {
|
||||
next = &summaries[idx+1]
|
||||
}
|
||||
return prev, next, nil
|
||||
}
|
||||
Reference in New Issue
Block a user