package mongodb import ( "context" "errors" "github.com/globalsign/mgo" "github.com/globalsign/mgo/bson" "sort" "strconv" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/repositories" ) type characterRepository struct { characters *mgo.Collection cidCounter *counter restoreIDs bool } func newCharacterRepository(db *mgo.Database, restoreIDs bool) (repositories.CharacterRepository, error) { collection := db.C("common.characters") err := collection.EnsureIndexKey("name") if err != nil { return nil, err } err = collection.EnsureIndexKey("shortName") if err != nil { return nil, err } err = collection.EnsureIndexKey("author") if err != nil { return nil, err } err = collection.EnsureIndex(mgo.Index{ Key: []string{"nicks"}, Unique: true, DropDups: true, }) if err != nil { return nil, err } err = collection.EnsureIndex(mgo.Index{ Key: []string{"$text:description"}, }) if err != nil { return nil, err } return &characterRepository{ restoreIDs: restoreIDs, characters: collection, cidCounter: newCounter(db, "auto_increment", "Character"), }, nil } func (r *characterRepository) Find(ctx context.Context, id string) (*models.Character, error) { character := new(models.Character) err := r.characters.FindId(id).One(character) if err != nil { return nil, err } return character, nil } func (r *characterRepository) FindNick(ctx context.Context, nick string) (*models.Character, error) { character := new(models.Character) err := r.characters.Find(bson.M{"nick": nick}).One(character) if err != nil { return nil, err } return character, nil } func (r *characterRepository) FindName(ctx context.Context, name string) (*models.Character, error) { query := bson.M{ "$or": []bson.M{ {"shortName": name}, {"name": name}, }, } // Look for all characters matching query characters := make([]*models.Character, 0, 8) err := r.characters.Find(query).All(&characters) if err != nil { if err == mgo.ErrNotFound { return nil, repositories.ErrNotFound } return nil, err } else if len(characters) == 0 { return nil, repositories.ErrNotFound } // Prioritize exact match for _, character := range characters { if character.Name == name { return character, nil } } return characters[0], nil } func (r *characterRepository) List(ctx context.Context, filter models.CharacterFilter) ([]*models.Character, error) { query := bson.M{} if filter.Author != nil { query["author"] = *filter.Author } if len(filter.IDs) > 0 { query["_id"] = bson.M{"$in": filter.IDs} } if len(filter.Nicks) > 0 { query["nicks"] = bson.M{"$in": filter.Nicks} } if len(filter.Names) > 0 { query["$or"] = []bson.M{ {"name": bson.M{"$in": filter.Names}}, {"shortName": bson.M{"$in": filter.Names}}, } } if filter.Search != nil { query["$text"] = bson.M{"$search": *filter.Search} } characters := make([]*models.Character, 0, 32) err := r.characters.Find(query).Limit(filter.Limit).All(&characters) if err != nil { if err == mgo.ErrNotFound { return characters, nil } return nil, err } sort.Slice(characters, func(i, j int) bool { ni, _ := strconv.Atoi(characters[i].ID[1:]) nj, _ := strconv.Atoi(characters[j].ID[1:]) return ni < nj }) return characters, nil } func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) { if !r.restoreIDs { nextId, err := r.cidCounter.Increment(1) if err != nil { return nil, err } character.ID = "C" + strconv.Itoa(nextId) } else { n, err := strconv.Atoi(character.ID[1:]) if err != nil { return nil, err } err = r.cidCounter.Bump(n) if err != nil { return nil, err } } err := r.characters.Insert(&character) if err != nil { return nil, err } return &character, nil } func (r *characterRepository) Update(ctx context.Context, character models.Character, update models.CharacterUpdate) (*models.Character, error) { updateBson := bson.M{} if update.Name != nil { updateBson["name"] = *update.Name character.Name = *update.Name } if update.ShortName != nil { updateBson["shortName"] = *update.ShortName character.ShortName = *update.ShortName } if update.Description != nil { updateBson["description"] = *update.Description character.Description = *update.Description } err := r.characters.UpdateId(character.ID, bson.M{"$set": updateBson}) if err != nil { return nil, err } return &character, nil } func (r *characterRepository) AddNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) { if character.HasNick(nick) { return nil, errors.New("nick already exist") } match := bson.M{ "_id": character.ID, "nicks": bson.M{"$ne": nick}, } err := r.characters.Update(match, bson.M{"$push": bson.M{"nicks": nick}}) if err == mgo.ErrNotFound { return nil, repositories.ErrNotFound } else if err != nil { return nil, err } newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1) copy(newNicks, character.Nicks) character.Nicks = append(newNicks, nick) return &character, nil } func (r *characterRepository) RemoveNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) { if !character.HasNick(nick) { return nil, errors.New("nick does not exist") } match := bson.M{ "_id": character.ID, "nicks": nick, } err := r.characters.Update(match, bson.M{"$pull": bson.M{"nicks": nick}}) if err == mgo.ErrNotFound { return nil, repositories.ErrNotFound } else if mErr, ok := err.(*mgo.LastError); ok && mErr.Code == 11000 { return nil, errors.New("The nick belongs to another character already") } else if err != nil { return nil, err } newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1) copy(newNicks, character.Nicks) for i := range newNicks { if newNicks[i] == nick { newNicks = append(newNicks[:i], newNicks[i+1:]...) break } } character.Nicks = newNicks return &character, nil } func (r *characterRepository) Delete(ctx context.Context, character models.Character) error { return r.characters.RemoveId(character.ID) }