From 56e74239d6b34df8f30ef046f0b0ff4ff0866a71 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Sun, 14 Jun 2015 23:53:32 -0800 Subject: first commit --- store/redis.go | 75 ++++++ store/redis_test.go | 59 +++++ store/sql_audit_store.go | 88 +++++++ store/sql_audit_store_test.go | 40 +++ store/sql_channel_store.go | 554 ++++++++++++++++++++++++++++++++++++++++ store/sql_channel_store_test.go | 493 +++++++++++++++++++++++++++++++++++ store/sql_post_store.go | 410 +++++++++++++++++++++++++++++ store/sql_post_store_test.go | 481 ++++++++++++++++++++++++++++++++++ store/sql_session_store.go | 176 +++++++++++++ store/sql_session_store_test.go | 134 ++++++++++ store/sql_store.go | 372 +++++++++++++++++++++++++++ store/sql_store_test.go | 83 ++++++ store/sql_team_store.go | 195 ++++++++++++++ store/sql_team_store_test.go | 155 +++++++++++ store/sql_user_store.go | 367 ++++++++++++++++++++++++++ store/sql_user_store_test.go | 276 ++++++++++++++++++++ store/store.go | 94 +++++++ 17 files changed, 4052 insertions(+) create mode 100644 store/redis.go create mode 100644 store/redis_test.go create mode 100644 store/sql_audit_store.go create mode 100644 store/sql_audit_store_test.go create mode 100644 store/sql_channel_store.go create mode 100644 store/sql_channel_store_test.go create mode 100644 store/sql_post_store.go create mode 100644 store/sql_post_store_test.go create mode 100644 store/sql_session_store.go create mode 100644 store/sql_session_store_test.go create mode 100644 store/sql_store.go create mode 100644 store/sql_store_test.go create mode 100644 store/sql_team_store.go create mode 100644 store/sql_team_store_test.go create mode 100644 store/sql_user_store.go create mode 100644 store/sql_user_store_test.go create mode 100644 store/store.go (limited to 'store') diff --git a/store/redis.go b/store/redis.go new file mode 100644 index 000000000..262040d43 --- /dev/null +++ b/store/redis.go @@ -0,0 +1,75 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + l4g "code.google.com/p/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "gopkg.in/redis.v2" + "strings" + "time" +) + +var client *redis.Client + +func RedisClient() *redis.Client { + + if client == nil { + + addr := utils.Cfg.RedisSettings.DataSource + + client = redis.NewTCPClient(&redis.Options{ + Addr: addr, + Password: "", + DB: 0, + PoolSize: utils.Cfg.RedisSettings.MaxOpenConns, + }) + + l4g.Info("Pinging redis at '%v'", addr) + pong, err := client.Ping().Result() + + if err != nil { + l4g.Critical("Failed to open redis connection to '%v' err:%v", addr, err) + time.Sleep(time.Second) + panic("Failed to open redis connection " + err.Error()) + } + + if pong != "PONG" { + l4g.Critical("Failed to ping redis connection to '%v' err:%v", addr, err) + time.Sleep(time.Second) + panic("Failed to open ping connection " + err.Error()) + } + } + + return client +} + +func RedisClose() { + l4g.Info("Closing redis") + + if client != nil { + client.Close() + client = nil + } +} + +func PublishAndForget(message *model.Message) { + + go func() { + c := RedisClient() + result := c.Publish(message.TeamId, message.ToJson()) + if result.Err() != nil { + l4g.Error("Failed to publish message err=%v, payload=%v", result.Err(), message.ToJson()) + } + }() +} + +func GetMessageFromPayload(m interface{}) *model.Message { + if msg, found := m.(*redis.Message); found { + return model.MessageFromJson(strings.NewReader(msg.Payload)) + } else { + return nil + } +} diff --git a/store/redis_test.go b/store/redis_test.go new file mode 100644 index 000000000..11bd9ca6a --- /dev/null +++ b/store/redis_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "fmt" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "testing" +) + +func TestRedis(t *testing.T) { + utils.LoadConfig("config.json") + + c := RedisClient() + + if c == nil { + t.Fatal("should have a valid redis connection") + } + + pubsub := c.PubSub() + defer pubsub.Close() + + m := model.NewMessage(model.NewId(), model.NewId(), model.NewId(), model.ACTION_TYPING) + m.Add("RootId", model.NewId()) + + err := pubsub.Subscribe(m.TeamId) + if err != nil { + t.Fatal(err) + } + + // should be the subscribe success message + // lets gobble that up + if _, err := pubsub.Receive(); err != nil { + t.Fatal(err) + } + + PublishAndForget(m) + + fmt.Println("here1") + + if msg, err := pubsub.Receive(); err != nil { + t.Fatal(err) + } else { + + rmsg := GetMessageFromPayload(msg) + + if m.TeamId != rmsg.TeamId { + t.Fatal("Ids do not match") + } + + if m.Props["RootId"] != rmsg.Props["RootId"] { + t.Fatal("Ids do not match") + } + } + + RedisClose() +} diff --git a/store/sql_audit_store.go b/store/sql_audit_store.go new file mode 100644 index 000000000..dd9312007 --- /dev/null +++ b/store/sql_audit_store.go @@ -0,0 +1,88 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" +) + +type SqlAuditStore struct { + *SqlStore +} + +func NewSqlAuditStore(sqlStore *SqlStore) AuditStore { + s := &SqlAuditStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Audit{}, "Audits").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("Action").SetMaxSize(64) + table.ColMap("ExtraInfo").SetMaxSize(128) + table.ColMap("IpAddress").SetMaxSize(64) + table.ColMap("SessionId").SetMaxSize(26) + } + + return s +} + +func (s SqlAuditStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlAuditStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_user_id", "Audits", "UserId") +} + +func (s SqlAuditStore) Save(audit *model.Audit) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + audit.Id = model.NewId() + audit.CreateAt = model.GetMillis() + + if err := s.GetMaster().Insert(audit); err != nil { + result.Err = model.NewAppError("SqlAuditStore.Save", + "We encounted an error saving the audit", "user_id="+ + audit.UserId+" action="+audit.Action) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlAuditStore) Get(user_id string, limit int) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if limit > 1000 { + limit = 1000 + result.Err = model.NewAppError("SqlAuditStore.Get", "Limit exceeded for paging", "user_id="+user_id) + storeChannel <- result + close(storeChannel) + return + } + + var audits model.Audits + if _, err := s.GetReplica().Select(&audits, "SELECT * FROM Audits WHERE UserId = ? ORDER BY CreateAt DESC LIMIT ?", + user_id, limit); err != nil { + result.Err = model.NewAppError("SqlAuditStore.Get", "We encounted an error finding the audits", "user_id="+user_id) + } else { + result.Data = audits + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_audit_store_test.go b/store/sql_audit_store_test.go new file mode 100644 index 000000000..3e6f22730 --- /dev/null +++ b/store/sql_audit_store_test.go @@ -0,0 +1,40 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestSqlAuditStore(t *testing.T) { + Setup() + + audit := &model.Audit{UserId: model.NewId(), IpAddress: "ipaddress", Action: "Action"} + <-store.Audit().Save(audit) + <-store.Audit().Save(audit) + <-store.Audit().Save(audit) + audit.ExtraInfo = "extra" + <-store.Audit().Save(audit) + + c := store.Audit().Get(audit.UserId, 100) + result := <-c + audits := result.Data.(model.Audits) + + if len(audits) != 4 { + t.Fatal("Failed to save and retrieve 4 audit logs") + } + + if audits[0].ExtraInfo != "extra" { + t.Fatal("Failed to save property for extra info") + } + + c = store.Audit().Get("missing", 100) + result = <-c + audits = result.Data.(model.Audits) + + if len(audits) != 0 { + t.Fatal("Should have returned empty because user_id is missing") + } +} diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go new file mode 100644 index 000000000..592657c1c --- /dev/null +++ b/store/sql_channel_store.go @@ -0,0 +1,554 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "strings" +) + +type SqlChannelStore struct { + *SqlStore +} + +func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore { + s := &SqlChannelStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Channel{}, "Channels").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("TeamId").SetMaxSize(26) + table.ColMap("Type").SetMaxSize(1) + table.ColMap("DisplayName").SetMaxSize(64) + table.ColMap("Name").SetMaxSize(64) + table.SetUniqueTogether("Name", "TeamId") + table.ColMap("Description").SetMaxSize(1024) + + tablem := db.AddTableWithName(model.ChannelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId") + tablem.ColMap("ChannelId").SetMaxSize(26) + tablem.ColMap("UserId").SetMaxSize(26) + tablem.ColMap("Roles").SetMaxSize(64) + tablem.ColMap("NotifyLevel").SetMaxSize(20) + } + + return s +} + +func (s SqlChannelStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlChannelStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_team_id", "Channels", "TeamId") +} + +func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if len(channel.Id) > 0 { + result.Err = model.NewAppError("SqlChannelStore.Save", + "Must call update for exisiting channel", "id="+channel.Id) + storeChannel <- result + close(storeChannel) + return + } + + channel.PreSave() + if result.Err = channel.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if count, err := s.GetMaster().SelectInt("SELECT COUNT(0) FROM Channels WHERE TeamId = ? AND DeleteAt = 0 AND (Type ='O' || Type ='P')", channel.TeamId); err != nil { + result.Err = model.NewAppError("SqlChannelStore.Save", "Failed to get current channel count", "teamId="+channel.TeamId+", "+err.Error()) + storeChannel <- result + close(storeChannel) + return + } else if count > 150 { + result.Err = model.NewAppError("SqlChannelStore.Save", "You've reached the limit of the number of allowed channels.", "teamId="+channel.TeamId) + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(channel); err != nil { + if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Name'") { + result.Err = model.NewAppError("SqlChannelStore.Save", "A channel with that name already exists", "id="+channel.Id+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlChannelStore.Save", "We couldn't save the channel", "id="+channel.Id+", "+err.Error()) + } + } else { + result.Data = channel + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) Update(channel *model.Channel) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + channel.PreUpdate() + + if result.Err = channel.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if count, err := s.GetMaster().Update(channel); err != nil { + if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Name'") { + result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that name already exists", "id="+channel.Id+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlChannelStore.Update", "We encounted an error updating the channel", "id="+channel.Id+", "+err.Error()) + } + } else if count != 1 { + result.Err = model.NewAppError("SqlChannelStore.Update", "We couldn't update the channel", "id="+channel.Id) + } else { + result.Data = channel + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) Get(id string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if obj, err := s.GetReplica().Get(model.Channel{}, id); err != nil { + result.Err = model.NewAppError("SqlChannelStore.Get", "We encounted an error finding the channel", "id="+id+", "+err.Error()) + } else if obj == nil { + result.Err = model.NewAppError("SqlChannelStore.Get", "We couldn't find the existing channel", "id="+id) + } else { + result.Data = obj.(*model.Channel) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) Delete(channelId string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("Update Channels SET DeleteAt = ?, UpdateAt = ? WHERE Id = ?", time, time, channelId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.Delete", "We couldn't delete the channel", "id="+channelId+", err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +type channelWithMember struct { + model.Channel + model.ChannelMember +} + +func (s SqlChannelStore) GetChannels(teamId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var data []channelWithMember + _, err := s.GetReplica().Select(&data, "SELECT * FROM Channels, ChannelMembers WHERE Id = ChannelId AND TeamId = ? AND UserId = ? AND DeleteAt = 0 ORDER BY DisplayName", teamId, userId) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetChannels", "We couldn't get the channels", "teamId="+teamId+", userId="+userId+", err="+err.Error()) + } else { + channels := &model.ChannelList{make([]*model.Channel, len(data)), make(map[string]*model.ChannelMember)} + for i := range data { + v := data[i] + channels.Channels[i] = &v.Channel + channels.Members[v.Channel.Id] = &v.ChannelMember + } + + if len(channels.Channels) == 0 { + result.Err = model.NewAppError("SqlChannelStore.GetChannels", "No channels were found", "teamId="+teamId+", userId="+userId) + } else { + result.Data = channels + } + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var data []*model.Channel + _, err := s.GetReplica().Select(&data, + `SELECT + * + FROM + Channels + WHERE + TeamId = ? + AND Type IN ("O") + AND DeleteAt = 0 + AND Id NOT IN (SELECT + Channels.Id + FROM + Channels, + ChannelMembers + WHERE + Id = ChannelId + AND TeamId = ? + AND UserId = ? + AND DeleteAt = 0) + ORDER BY DisplayName`, + teamId, teamId, userId) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetMoreChannels", "We couldn't get the channels", "teamId="+teamId+", userId="+userId+", err="+err.Error()) + } else { + result.Data = &model.ChannelList{data, make(map[string]*model.ChannelMember)} + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) GetByName(teamId string, name string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + channel := model.Channel{} + + if err := s.GetReplica().SelectOne(&channel, "SELECT * FROM Channels WHERE TeamId=? AND Name=? AND DeleteAt = 0", teamId, name); err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetByName", "We couldn't find the existing channel", "teamId="+teamId+", "+"name="+name+", "+err.Error()) + } else { + result.Data = &channel + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if result.Err = member.IsValid(); result.Err != nil { + storeChannel <- result + return + } + + if err := s.GetMaster().Insert(member); err != nil { + if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'ChannelId'") { + result.Err = model.NewAppError("SqlChannelStore.SaveMember", "A channel member with that id already exists", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlChannelStore.SaveMember", "We couldn't save the channel member", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error()) + } + } else { + result.Data = member + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) GetMembers(channelId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var members []model.ChannelMember + _, err := s.GetReplica().Select(&members, "SELECT * FROM ChannelMembers WHERE ChannelId = ?", channelId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetMembers", "We couldn't get the channel members", "channel_id="+channelId+err.Error()) + } else { + result.Data = members + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var member model.ChannelMember + err := s.GetReplica().SelectOne(&member, "SELECT * FROM ChannelMembers WHERE ChannelId = ? AND UserId = ?", channelId, userId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetMember", "We couldn't get the channel member", "channel_id="+channelId+"user_id="+userId+","+err.Error()) + } else { + result.Data = member + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var members []model.ExtraMember + _, err := s.GetReplica().Select(&members, "SELECT Id, FullName, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = ? LIMIT ?", channelId, limit) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetExtraMembers", "We couldn't get the extra info for channel members", "channel_id="+channelId+", "+err.Error()) + } else { + for i, _ := range members { + members[i].Sanitize(utils.SanitizeOptions) + } + result.Data = members + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) RemoveMember(channelId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE ChannelId = ? AND UserId = ?", channelId, userId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "We couldn't remove the channel member", "channel_id="+channelId+", user_id="+userId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + count, err := s.GetReplica().SelectInt( + `SELECT + COUNT(0) + FROM + Channels, + ChannelMembers + WHERE + Channels.Id = ChannelMembers.ChannelId + AND Channels.TeamId = ? + AND Channels.DeleteAt = 0 + AND ChannelMembers.ChannelId = ? + AND ChannelMembers.UserId = ?`, + teamId, channelId, userId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.CheckPermissionsTo", "We couldn't check the permissions", "channel_id="+channelId+", user_id="+userId+", "+err.Error()) + } else { + result.Data = count + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + channelId, err := s.GetReplica().SelectStr( + `SELECT + Channels.Id + FROM + Channels, + ChannelMembers + WHERE + Channels.Id = ChannelMembers.ChannelId + AND Channels.TeamId = ? + AND Channels.Name = ? + AND Channels.DeleteAt = 0 + AND ChannelMembers.UserId = ?`, + teamId, channelName, userId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.CheckPermissionsToByName", "We couldn't check the permissions", "channel_id="+channelName+", user_id="+userId+", "+err.Error()) + } else { + result.Data = channelId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) CheckOpenChannelPermissions(teamId string, channelId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + count, err := s.GetReplica().SelectInt( + `SELECT + COUNT(0) + FROM + Channels + WHERE + Channels.Id = ? + AND Channels.TeamId = ? + AND Channels.Type = ?`, + channelId, teamId, model.CHANNEL_OPEN) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.CheckOpenChannelPermissions", "We couldn't check the permissions", "channel_id="+channelId+", "+err.Error()) + } else { + result.Data = count + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec( + `UPDATE + ChannelMembers, Channels + SET + ChannelMembers.MentionCount = 0, + ChannelMembers.MsgCount = Channels.TotalMsgCount, + ChannelMembers.LastViewedAt = Channels.LastPostAt + WHERE + Channels.Id = ChannelMembers.ChannelId + AND UserId = ? + AND ChannelId = ?`, + userId, channelId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.UpdateLastViewedAt", "We couldn't update the last viewed at time", "channel_id="+channelId+", user_id="+userId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec( + `UPDATE + ChannelMembers + SET + MentionCount = MentionCount + 1 + WHERE + UserId = ? + AND ChannelId = ?`, + userId, channelId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.IncrementMentionCount", "We couldn't increment the mention count", "channel_id="+channelId+", user_id="+userId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec( + `UPDATE + ChannelMembers + SET + NotifyLevel = ? + WHERE + UserId = ? + AND ChannelId = ?`, + notifyLevel, userId, channelId) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.UpdateNotifyLevel", "We couldn't update the notify level", "channel_id="+channelId+", user_id="+userId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go new file mode 100644 index 000000000..ce4ff75f0 --- /dev/null +++ b/store/sql_channel_store_test.go @@ -0,0 +1,493 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestChannelStoreSave(t *testing.T) { + Setup() + + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "Name" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + + if err := (<-store.Channel().Save(&o1)).Err; err != nil { + t.Fatal("couldn't save item", err) + } + + if err := (<-store.Channel().Save(&o1)).Err; err == nil { + t.Fatal("shouldn't be able to update from save") + } + + o1.Id = "" + if err := (<-store.Channel().Save(&o1)).Err; err == nil { + t.Fatal("should be unique name") + } + + for i := 0; i < 150; i++ { + o1.Id = "" + o1.Name = "a" + model.NewId() + "b" + if err := (<-store.Channel().Save(&o1)).Err; err != nil { + t.Fatal("couldn't save item", err) + } + } + + o1.Id = "" + o1.Name = "a" + model.NewId() + "b" + if err := (<-store.Channel().Save(&o1)).Err; err == nil { + t.Fatal("should be the limit") + } +} + +func TestChannelStoreUpdate(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Name" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + if err := (<-store.Channel().Update(&o1)).Err; err != nil { + t.Fatal(err) + } + + o1.Id = "missing" + if err := (<-store.Channel().Update(&o1)).Err; err == nil { + t.Fatal("Update should have failed because of missing key") + } + + o1.Id = model.NewId() + if err := (<-store.Channel().Update(&o1)).Err; err == nil { + t.Fatal("Update should have faile because id change") + } +} + +func TestChannelStoreGet(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Name" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + if r1 := <-store.Channel().Get(o1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.Channel).ToJson() != o1.ToJson() { + t.Fatal("invalid returned channel") + } + } + + if err := (<-store.Channel().Get("")).Err; err == nil { + t.Fatal("Missing id should have failed") + } +} + +func TestChannelStoreDelete(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + o2 := model.Channel{} + o2.TeamId = o1.TeamId + o2.DisplayName = "Channel2" + o2.Name = "a" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o2) + + o3 := model.Channel{} + o3.TeamId = o1.TeamId + o3.DisplayName = "Channel3" + o3.Name = "a" + model.NewId() + "b" + o3.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o3) + + o4 := model.Channel{} + o4.TeamId = o1.TeamId + o4.DisplayName = "Channel4" + o4.Name = "a" + model.NewId() + "b" + o4.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o4) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m1) + + m2 := model.ChannelMember{} + m2.ChannelId = o2.Id + m2.UserId = m1.UserId + m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m2) + + if r := <-store.Channel().Delete(o1.Id, model.GetMillis()); r.Err != nil { + t.Fatal(r.Err) + } + + if r := <-store.Channel().Get(o1.Id); r.Data.(*model.Channel).DeleteAt == 0 { + t.Fatal("should have been deleted") + } + + if r := <-store.Channel().Delete(o3.Id, model.GetMillis()); r.Err != nil { + t.Fatal(r.Err) + } + + cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId) + list := cresult.Data.(*model.ChannelList) + + if len(list.Channels) != 1 { + t.Fatal("invalid number of channels") + } + + cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId) + list = cresult.Data.(*model.ChannelList) + + if len(list.Channels) != 1 { + t.Fatal("invalid number of channels") + } +} + +func TestChannelStoreGetByName(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Name" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + if r1 := <-store.Channel().GetByName(o1.TeamId, o1.Name); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.Channel).ToJson() != o1.ToJson() { + t.Fatal("invalid returned channel") + } + } + + if err := (<-store.Channel().GetByName(o1.TeamId, "")).Err; err == nil { + t.Fatal("Missing id should have failed") + } +} + +func TestChannelMemberStore(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + u1.FullName = model.NewId() + <-store.User().Save(&u1) + + u2 := model.User{} + u2.TeamId = model.NewId() + u2.Email = model.NewId() + u2.FullName = model.NewId() + <-store.User().Save(&u2) + + o1 := model.ChannelMember{} + o1.ChannelId = model.NewId() + o1.UserId = u1.Id + o1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&o1) + + o2 := model.ChannelMember{} + o2.ChannelId = o1.ChannelId + o2.UserId = u2.Id + o2.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&o2) + + members := (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember) + if len(members) != 2 { + t.Fatal("should have saved 2 members") + } + + <-store.Channel().RemoveMember(o2.ChannelId, o2.UserId) + + members = (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember) + if len(members) != 1 { + t.Fatal("should have removed 1 member") + } + + member := (<-store.Channel().GetMember(o1.ChannelId, o1.UserId)).Data.(model.ChannelMember) + if member.ChannelId != o1.ChannelId { + t.Fatal("should have go member") + } + + extraMembers := (<-store.Channel().GetExtraMembers(o1.ChannelId, 20)).Data.([]model.ExtraMember) + if len(extraMembers) != 1 { + t.Fatal("should have 1 extra members") + } + + if err := (<-store.Channel().SaveMember(&o1)).Err; err == nil { + t.Fatal("Should have been a duplicate") + } +} + +func TestChannelStorePermissionsTo(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m1) + + count := (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, m1.UserId)).Data.(int64) + if count != 1 { + t.Fatal("should have permissions") + } + + count = (<-store.Channel().CheckPermissionsTo("junk", o1.Id, m1.UserId)).Data.(int64) + if count != 0 { + t.Fatal("shouldn't have permissions") + } + + count = (<-store.Channel().CheckPermissionsTo(o1.TeamId, "junk", m1.UserId)).Data.(int64) + if count != 0 { + t.Fatal("shouldn't have permissions") + } + + count = (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, "junk")).Data.(int64) + if count != 0 { + t.Fatal("shouldn't have permissions") + } + + channelId := (<-store.Channel().CheckPermissionsToByName(o1.TeamId, o1.Name, m1.UserId)).Data.(string) + if channelId != o1.Id { + t.Fatal("should have permissions") + } + + channelId = (<-store.Channel().CheckPermissionsToByName(o1.TeamId, "missing", m1.UserId)).Data.(string) + if channelId != "" { + t.Fatal("should not have permissions") + } +} + +func TestChannelStoreOpenChannelPermissionsTo(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + count := (<-store.Channel().CheckOpenChannelPermissions(o1.TeamId, o1.Id)).Data.(int64) + if count != 1 { + t.Fatal("should have permissions") + } + + count = (<-store.Channel().CheckOpenChannelPermissions("junk", o1.Id)).Data.(int64) + if count != 0 { + t.Fatal("shouldn't have permissions") + } + + count = (<-store.Channel().CheckOpenChannelPermissions(o1.TeamId, "junk")).Data.(int64) + if count != 0 { + t.Fatal("shouldn't have permissions") + } +} + +func TestChannelStoreGetChannels(t *testing.T) { + Setup() + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Channel2" + o2.Name = "a" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o2) + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m1) + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = model.NewId() + m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m2) + + m3 := model.ChannelMember{} + m3.ChannelId = o2.Id + m3.UserId = model.NewId() + m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m3) + + cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId) + list := cresult.Data.(*model.ChannelList) + + if list.Channels[0].Id != o1.Id { + t.Fatal("missing channel") + } +} + +func TestChannelStoreGetMoreChannels(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o1) + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Channel2" + o2.Name = "a" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o2) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m1) + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = model.NewId() + m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m2) + + m3 := model.ChannelMember{} + m3.ChannelId = o2.Id + m3.UserId = model.NewId() + m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m3) + + o3 := model.Channel{} + o3.TeamId = o1.TeamId + o3.DisplayName = "ChannelA" + o3.Name = "a" + model.NewId() + "b" + o3.Type = model.CHANNEL_OPEN + <-store.Channel().Save(&o3) + + o4 := model.Channel{} + o4.TeamId = o1.TeamId + o4.DisplayName = "ChannelB" + o4.Name = "a" + model.NewId() + "b" + o4.Type = model.CHANNEL_PRIVATE + <-store.Channel().Save(&o4) + + o5 := model.Channel{} + o5.TeamId = o1.TeamId + o5.DisplayName = "ChannelC" + o5.Name = "a" + model.NewId() + "b" + o5.Type = model.CHANNEL_PRIVATE + <-store.Channel().Save(&o5) + + cresult := <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId) + list := cresult.Data.(*model.ChannelList) + + if len(list.Channels) != 1 { + t.Fatal("wrong list") + } + + if list.Channels[0].Name != o3.Name { + t.Fatal("missing channel") + } +} + +func TestChannelStoreUpdateLastViewedAt(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + o1.TotalMsgCount = 25 + <-store.Channel().Save(&o1) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m1) + + err := (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, m1.UserId)).Err + if err != nil { + t.Fatal("failed to update") + } + + err = (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, "missing id")).Err + if err != nil { + t.Fatal("failed to update") + } +} + +func TestChannelStoreIncrementMentionCount(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + o1.TotalMsgCount = 25 + <-store.Channel().Save(&o1) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m1) + + err := (<-store.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId)).Err + if err != nil { + t.Fatal("failed to update") + } + + err = (<-store.Channel().IncrementMentionCount(m1.ChannelId, "missing id")).Err + if err != nil { + t.Fatal("failed to update") + } + + err = (<-store.Channel().IncrementMentionCount("missing id", m1.UserId)).Err + if err != nil { + t.Fatal("failed to update") + } + + err = (<-store.Channel().IncrementMentionCount("missing id", "missing id")).Err + if err != nil { + t.Fatal("failed to update") + } +} diff --git a/store/sql_post_store.go b/store/sql_post_store.go new file mode 100644 index 000000000..0ceebc02f --- /dev/null +++ b/store/sql_post_store.go @@ -0,0 +1,410 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "fmt" + "github.com/mattermost/platform/model" + "strconv" + "strings" +) + +type SqlPostStore struct { + *SqlStore +} + +func NewSqlPostStore(sqlStore *SqlStore) PostStore { + s := &SqlPostStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Post{}, "Posts").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("ChannelId").SetMaxSize(26) + table.ColMap("RootId").SetMaxSize(26) + table.ColMap("ParentId").SetMaxSize(26) + table.ColMap("Message").SetMaxSize(4000) + table.ColMap("Type").SetMaxSize(26) + table.ColMap("Hashtags").SetMaxSize(1000) + table.ColMap("Props").SetMaxSize(4000) + table.ColMap("Filenames").SetMaxSize(4000) + } + + return s +} + +func (s SqlPostStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlPostStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_update_at", "Posts", "UpdateAt") + s.CreateIndexIfNotExists("idx_create_at", "Posts", "CreateAt") + s.CreateIndexIfNotExists("idx_channel_id", "Posts", "ChannelId") + s.CreateIndexIfNotExists("idx_root_id", "Posts", "RootId") + + s.CreateFullTextIndexIfNotExists("idx_message_txt", "Posts", "Message") + s.CreateFullTextIndexIfNotExists("idx_hashtags_txt", "Posts", "Hashtags") +} + +func (s SqlPostStore) Save(post *model.Post) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if len(post.Id) > 0 { + result.Err = model.NewAppError("SqlPostStore.Save", + "You cannot update an existing Post", "id="+post.Id) + storeChannel <- result + close(storeChannel) + return + } + + post.PreSave() + if result.Err = post.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(post); err != nil { + result.Err = model.NewAppError("SqlPostStore.Save", "We couldn't save the Post", "id="+post.Id+", "+err.Error()) + } else { + time := model.GetMillis() + s.GetMaster().Exec("UPDATE Channels SET LastPostAt = ?, TotalMsgCount = TotalMsgCount + 1 WHERE Id = ?", time, post.ChannelId) + + if len(post.RootId) > 0 { + s.GetMaster().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", time, post.RootId) + } + + result.Data = post + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPostStore) Update(oldPost *model.Post, newMessage string, newHashtags string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + editPost := *oldPost + editPost.Message = newMessage + editPost.UpdateAt = model.GetMillis() + editPost.Hashtags = newHashtags + + oldPost.DeleteAt = editPost.UpdateAt + oldPost.UpdateAt = editPost.UpdateAt + oldPost.OriginalId = oldPost.Id + oldPost.Id = model.NewId() + + if result.Err = editPost.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if _, err := s.GetMaster().Update(&editPost); err != nil { + result.Err = model.NewAppError("SqlPostStore.Update", "We couldn't update the Post", "id="+editPost.Id+", "+err.Error()) + } else { + time := model.GetMillis() + s.GetMaster().Exec("UPDATE Channels SET LastPostAt = ? WHERE Id = ?", time, editPost.ChannelId) + + if len(editPost.RootId) > 0 { + s.GetMaster().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", time, editPost.RootId) + } + + // mark the old post as deleted + s.GetMaster().Insert(oldPost) + + result.Data = &editPost + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPostStore) Get(id string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + pl := &model.PostList{} + + var post model.Post + err := s.GetReplica().SelectOne(&post, "SELECT * FROM Posts WHERE Id = ? AND DeleteAt = 0", id) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.GetPost", "We couldn't get the post", "id="+id+err.Error()) + } + + if post.ImgCount > 0 { + post.Filenames = []string{} + for i := 0; int64(i) < post.ImgCount; i++ { + fileUrl := "/api/v1/files/get_image/" + post.ChannelId + "/" + post.Id + "/" + strconv.Itoa(i+1) + ".png" + post.Filenames = append(post.Filenames, fileUrl) + } + } + + pl.AddPost(&post) + pl.AddOrder(id) + + rootId := post.RootId + + if rootId == "" { + rootId = post.Id + } + + var posts []*model.Post + _, err = s.GetReplica().Select(&posts, "SELECT * FROM Posts WHERE (Id = ? OR RootId = ?) AND DeleteAt = 0", rootId, rootId) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.GetPost", "We couldn't get the post", "root_id="+rootId+err.Error()) + } else { + for _, p := range posts { + pl.AddPost(p) + } + } + + result.Data = pl + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +type etagPosts struct { + Id string + UpdateAt int64 +} + +func (s SqlPostStore) GetEtag(channelId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var et etagPosts + err := s.GetReplica().SelectOne(&et, "SELECT Id, UpdateAt FROM Posts WHERE ChannelId = ? ORDER BY UpdateAt DESC LIMIT 1", channelId) + if err != nil { + result.Data = fmt.Sprintf("%v.0.%v", model.ETAG_ROOT_VERSION, model.GetMillis()) + } else { + result.Data = fmt.Sprintf("%v.%v.%v", model.ETAG_ROOT_VERSION, et.Id, et.UpdateAt) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPostStore) Delete(postId string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("Update Posts SET DeleteAt = ?, UpdateAt = ? WHERE Id = ? OR ParentId = ? OR RootId = ?", time, time, postId, postId, postId) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.Delete", "We couldn't delete the post", "id="+postId+", err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPostStore) GetPosts(channelId string, offset int, limit int) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if limit > 1000 { + result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "Limit exceeded for paging", "channelId="+channelId) + storeChannel <- result + close(storeChannel) + return + } + + rpc := s.getRootPosts(channelId, offset, limit) + cpc := s.getParentsPosts(channelId, offset, limit) + + if rpr := <-rpc; rpr.Err != nil { + result.Err = rpr.Err + } else if cpr := <-cpc; cpr.Err != nil { + result.Err = cpr.Err + } else { + posts := rpr.Data.([]*model.Post) + parents := cpr.Data.([]*model.Post) + + list := &model.PostList{Order: make([]string, 0, len(posts))} + + for _, p := range posts { + if p.ImgCount > 0 { + p.Filenames = []string{} + for i := 0; int64(i) < p.ImgCount; i++ { + fileUrl := "/api/v1/files/get_image/" + p.ChannelId + "/" + p.Id + "/" + strconv.Itoa(i+1) + ".png" + p.Filenames = append(p.Filenames, fileUrl) + } + } + list.AddPost(p) + list.AddOrder(p.Id) + } + + for _, p := range parents { + if p.ImgCount > 0 { + p.Filenames = []string{} + for i := 0; int64(i) < p.ImgCount; i++ { + fileUrl := "/api/v1/files/get_image/" + p.ChannelId + "/" + p.Id + "/" + strconv.Itoa(i+1) + ".png" + p.Filenames = append(p.Filenames, fileUrl) + } + } + list.AddPost(p) + } + + list.MakeNonNil() + + result.Data = list + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPostStore) getRootPosts(channelId string, offset int, limit int) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var posts []*model.Post + _, err := s.GetReplica().Select(&posts, "SELECT * FROM Posts WHERE ChannelId = ? AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT ?,?", channelId, offset, limit) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "We couldn't get the posts for the channel", "channelId="+channelId+err.Error()) + } else { + result.Data = posts + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var posts []*model.Post + _, err := s.GetReplica().Select(&posts, + `SELECT + q2.* + FROM + Posts q2 + INNER JOIN + (SELECT DISTINCT + q3.RootId + FROM + (SELECT + RootId + FROM + Posts + WHERE + ChannelId = ? + AND DeleteAt = 0 + ORDER BY CreateAt DESC + LIMIT ?, ?) q3) q1 ON q1.RootId = q2.RootId + WHERE + ChannelId = ? + AND DeleteAt = 0 + ORDER BY CreateAt`, + channelId, offset, limit, channelId) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "We couldn't get the parent post for the channel", "channelId="+channelId+err.Error()) + } else { + result.Data = posts + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlPostStore) Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + searchType := "Message" + if isHashtagSearch { + searchType = "Hashtags" + } + + // @ has a speical meaning in INNODB FULLTEXT indexes and + // is reserved for calc'ing distances so you + // cannot escape it so we replace it. + terms = strings.Replace(terms, "@", " ", -1) + + searchQuery := fmt.Sprintf(`SELECT + * + FROM + Posts + WHERE + DeleteAt = 0 + AND ChannelId IN (SELECT + Id + FROM + Channels, + ChannelMembers + WHERE + Id = ChannelId AND TeamId = ? + AND UserId = ? + AND DeleteAt = 0) + AND MATCH (%s) AGAINST (? IN BOOLEAN MODE) + ORDER BY CreateAt DESC + LIMIT 100`, searchType) + + var posts []*model.Post + _, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error()) + } else { + + list := &model.PostList{Order: make([]string, 0, len(posts))} + + for _, p := range posts { + list.AddPost(p) + list.AddOrder(p.Id) + } + + list.MakeNonNil() + + result.Data = list + } + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go new file mode 100644 index 000000000..13dd6e5ed --- /dev/null +++ b/store/sql_post_store_test.go @@ -0,0 +1,481 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "strings" + "testing" + "time" +) + +func TestPostStoreSave(t *testing.T) { + Setup() + + o1 := model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + + if err := (<-store.Post().Save(&o1)).Err; err != nil { + t.Fatal("couldn't save item", err) + } + + if err := (<-store.Post().Save(&o1)).Err; err == nil { + t.Fatal("shouldn't be able to update from save") + } +} + +func TestPostStoreGet(t *testing.T) { + Setup() + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + + etag1 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string) + if strings.Index(etag1, model.ETAG_ROOT_VERSION+".0.") != 0 { + t.Fatal("Invalid Etag") + } + + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + + etag2 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string) + if strings.Index(etag2, model.ETAG_ROOT_VERSION+"."+o1.Id) != 0 { + t.Fatal("Invalid Etag") + } + + if r1 := <-store.Post().Get(o1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.PostList).Posts[o1.Id].CreateAt != o1.CreateAt { + t.Fatal("invalid returned post") + } + } + + if err := (<-store.Post().Get("123")).Err; err == nil { + t.Fatal("Missing id should have failed") + } +} + +func TestPostStoreUpdate(t *testing.T) { + Setup() + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "AAAAAAAAAAA" + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + + o2 := &model.Post{} + o2.ChannelId = o1.ChannelId + o2.UserId = model.NewId() + o2.Message = "a" + model.NewId() + "CCCCCCCCC" + o2.ParentId = o1.Id + o2.RootId = o1.Id + o2 = (<-store.Post().Save(o2)).Data.(*model.Post) + + o3 := &model.Post{} + o3.ChannelId = o1.ChannelId + o3.UserId = model.NewId() + o3.Message = "a" + model.NewId() + "QQQQQQQQQQ" + o3 = (<-store.Post().Save(o3)).Data.(*model.Post) + + ro1 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o1.Id] + ro2 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o2.Id] + ro6 := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id] + + if ro1.Message != o1.Message { + t.Fatal("Failed to save/get") + } + + msg := o1.Message + "BBBBBBBBBB" + if result := <-store.Post().Update(ro1, msg, ""); result.Err != nil { + t.Fatal(result.Err) + } + + msg2 := o2.Message + "DDDDDDD" + if result := <-store.Post().Update(ro2, msg2, ""); result.Err != nil { + t.Fatal(result.Err) + } + + msg3 := o3.Message + "WWWWWWW" + if result := <-store.Post().Update(ro6, msg3, "#hashtag"); result.Err != nil { + t.Fatal(result.Err) + } + + ro3 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o1.Id] + + if ro3.Message != msg { + t.Fatal("Failed to update/get") + } + + ro4 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o2.Id] + + if ro4.Message != msg2 { + t.Fatal("Failed to update/get") + } + + ro5 := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id] + + if ro5.Message != msg3 && ro5.Hashtags != "#hashtag" { + t.Fatal("Failed to update/get") + } + +} + +func TestPostStoreDelete(t *testing.T) { + Setup() + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + + etag1 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string) + if strings.Index(etag1, model.ETAG_ROOT_VERSION+".0.") != 0 { + t.Fatal("Invalid Etag") + } + + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + + if r1 := <-store.Post().Get(o1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.PostList).Posts[o1.Id].CreateAt != o1.CreateAt { + t.Fatal("invalid returned post") + } + } + + if r2 := <-store.Post().Delete(o1.Id, model.GetMillis()); r2.Err != nil { + t.Fatal(r2.Err) + } + + if r3 := (<-store.Post().Get(o1.Id)); r3.Err == nil { + t.Log(r3.Data) + t.Fatal("Missing id should have failed") + } + + etag2 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string) + if strings.Index(etag2, model.ETAG_ROOT_VERSION+"."+o1.Id) != 0 { + t.Fatal("Invalid Etag") + } +} + +func TestPostStoreDelete1Level(t *testing.T) { + Setup() + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + + o2 := &model.Post{} + o2.ChannelId = o1.ChannelId + o2.UserId = model.NewId() + o2.Message = "a" + model.NewId() + "b" + o2.ParentId = o1.Id + o2.RootId = o1.Id + o2 = (<-store.Post().Save(o2)).Data.(*model.Post) + + if r2 := <-store.Post().Delete(o1.Id, model.GetMillis()); r2.Err != nil { + t.Fatal(r2.Err) + } + + if r3 := (<-store.Post().Get(o1.Id)); r3.Err == nil { + t.Fatal("Deleted id should have failed") + } + + if r4 := (<-store.Post().Get(o2.Id)); r4.Err == nil { + t.Fatal("Deleted id should have failed") + } +} + +func TestPostStoreDelete2Level(t *testing.T) { + Setup() + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + + o2 := &model.Post{} + o2.ChannelId = o1.ChannelId + o2.UserId = model.NewId() + o2.Message = "a" + model.NewId() + "b" + o2.ParentId = o1.Id + o2.RootId = o1.Id + o2 = (<-store.Post().Save(o2)).Data.(*model.Post) + + o3 := &model.Post{} + o3.ChannelId = o1.ChannelId + o3.UserId = model.NewId() + o3.Message = "a" + model.NewId() + "b" + o3.ParentId = o2.Id + o3.RootId = o1.Id + o3 = (<-store.Post().Save(o3)).Data.(*model.Post) + + o4 := &model.Post{} + o4.ChannelId = model.NewId() + o4.UserId = model.NewId() + o4.Message = "a" + model.NewId() + "b" + o4 = (<-store.Post().Save(o4)).Data.(*model.Post) + + if r2 := <-store.Post().Delete(o1.Id, model.GetMillis()); r2.Err != nil { + t.Fatal(r2.Err) + } + + if r3 := (<-store.Post().Get(o1.Id)); r3.Err == nil { + t.Fatal("Deleted id should have failed") + } + + if r4 := (<-store.Post().Get(o2.Id)); r4.Err == nil { + t.Fatal("Deleted id should have failed") + } + + if r5 := (<-store.Post().Get(o3.Id)); r5.Err == nil { + t.Fatal("Deleted id should have failed") + } + + if r6 := <-store.Post().Get(o4.Id); r6.Err != nil { + t.Fatal(r6.Err) + } +} + +func TestPostStoreGetWithChildren(t *testing.T) { + Setup() + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + + o2 := &model.Post{} + o2.ChannelId = o1.ChannelId + o2.UserId = model.NewId() + o2.Message = "a" + model.NewId() + "b" + o2.ParentId = o1.Id + o2.RootId = o1.Id + o2 = (<-store.Post().Save(o2)).Data.(*model.Post) + + o3 := &model.Post{} + o3.ChannelId = o1.ChannelId + o3.UserId = model.NewId() + o3.Message = "a" + model.NewId() + "b" + o3.ParentId = o2.Id + o3.RootId = o1.Id + o3 = (<-store.Post().Save(o3)).Data.(*model.Post) + + if r1 := <-store.Post().Get(o1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + pl := r1.Data.(*model.PostList) + if len(pl.Posts) != 3 { + t.Fatal("invalid returned post") + } + } + + <-store.Post().Delete(o3.Id, model.GetMillis()) + + if r2 := <-store.Post().Get(o1.Id); r2.Err != nil { + t.Fatal(r2.Err) + } else { + pl := r2.Data.(*model.PostList) + if len(pl.Posts) != 2 { + t.Fatal("invalid returned post") + } + } + + <-store.Post().Delete(o2.Id, model.GetMillis()) + + if r3 := <-store.Post().Get(o1.Id); r3.Err != nil { + t.Fatal(r3.Err) + } else { + pl := r3.Data.(*model.PostList) + if len(pl.Posts) != 1 { + t.Fatal("invalid returned post") + } + } +} + +func TestPostStoreGetPostsWtihDetails(t *testing.T) { + Setup() + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o2 := &model.Post{} + o2.ChannelId = o1.ChannelId + o2.UserId = model.NewId() + o2.Message = "a" + model.NewId() + "b" + o2.ParentId = o1.Id + o2.RootId = o1.Id + o2 = (<-store.Post().Save(o2)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o2a := &model.Post{} + o2a.ChannelId = o1.ChannelId + o2a.UserId = model.NewId() + o2a.Message = "a" + model.NewId() + "b" + o2a.ParentId = o1.Id + o2a.RootId = o1.Id + o2a = (<-store.Post().Save(o2a)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o3 := &model.Post{} + o3.ChannelId = o1.ChannelId + o3.UserId = model.NewId() + o3.Message = "a" + model.NewId() + "b" + o3.ParentId = o1.Id + o3.RootId = o1.Id + o3 = (<-store.Post().Save(o3)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o4 := &model.Post{} + o4.ChannelId = o1.ChannelId + o4.UserId = model.NewId() + o4.Message = "a" + model.NewId() + "b" + o4 = (<-store.Post().Save(o4)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o5 := &model.Post{} + o5.ChannelId = o1.ChannelId + o5.UserId = model.NewId() + o5.Message = "a" + model.NewId() + "b" + o5.ParentId = o4.Id + o5.RootId = o4.Id + o5 = (<-store.Post().Save(o5)).Data.(*model.Post) + + r1 := (<-store.Post().GetPosts(o1.ChannelId, 0, 4)).Data.(*model.PostList) + + if r1.Order[0] != o5.Id { + t.Fatal("invalid order") + } + + if r1.Order[1] != o4.Id { + t.Fatal("invalid order") + } + + if r1.Order[2] != o3.Id { + t.Fatal("invalid order") + } + + if r1.Order[3] != o2a.Id { + t.Fatal("invalid order") + } + + if len(r1.Posts) != 6 { + t.Fatal("wrong size") + } + + if r1.Posts[o1.Id].Message != o1.Message { + t.Fatal("Missing parent") + } +} + +func TestPostStoreSearch(t *testing.T) { + Setup() + + teamId := model.NewId() + userId := model.NewId() + + c1 := &model.Channel{} + c1.TeamId = teamId + c1.DisplayName = "Channel1" + c1.Name = "a" + model.NewId() + "b" + c1.Type = model.CHANNEL_OPEN + c1 = (<-store.Channel().Save(c1)).Data.(*model.Channel) + + m1 := model.ChannelMember{} + m1.ChannelId = c1.Id + m1.UserId = userId + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + <-store.Channel().SaveMember(&m1) + + c2 := &model.Channel{} + c2.TeamId = teamId + c2.DisplayName = "Channel1" + c2.Name = "a" + model.NewId() + "b" + c2.Type = model.CHANNEL_OPEN + c2 = (<-store.Channel().Save(c2)).Data.(*model.Channel) + + o1 := &model.Post{} + o1.ChannelId = c1.Id + o1.UserId = model.NewId() + o1.Message = "corey mattermost new york" + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + + o2 := &model.Post{} + o2.ChannelId = c1.Id + o2.UserId = model.NewId() + o2.Message = "New Jersey is where John is from" + o2 = (<-store.Post().Save(o2)).Data.(*model.Post) + + o3 := &model.Post{} + o3.ChannelId = c2.Id + o3.UserId = model.NewId() + o3.Message = "New Jersey is where John is from corey new york" + o3 = (<-store.Post().Save(o3)).Data.(*model.Post) + + o4 := &model.Post{} + o4.ChannelId = c1.Id + o4.UserId = model.NewId() + o4.Hashtags = "#hashtag" + o4.Message = "message" + o4 = (<-store.Post().Save(o4)).Data.(*model.Post) + + o5 := &model.Post{} + o5.ChannelId = c1.Id + o5.UserId = model.NewId() + o5.Hashtags = "#secret #howdy" + o5 = (<-store.Post().Save(o5)).Data.(*model.Post) + + r1 := (<-store.Post().Search(teamId, userId, "corey", false)).Data.(*model.PostList) + if len(r1.Order) != 1 && r1.Order[0] != o1.Id { + t.Fatal("returned wrong serach result") + } + + r2 := (<-store.Post().Search(teamId, userId, "new york", false)).Data.(*model.PostList) + if len(r2.Order) != 2 && r2.Order[0] != o2.Id { + t.Fatal("returned wrong serach result") + } + + r3 := (<-store.Post().Search(teamId, userId, "new", false)).Data.(*model.PostList) + if len(r3.Order) != 2 && r3.Order[0] != o1.Id { + t.Fatal("returned wrong serach result") + } + + r4 := (<-store.Post().Search(teamId, userId, "john", false)).Data.(*model.PostList) + if len(r4.Order) != 1 && r4.Order[0] != o2.Id { + t.Fatal("returned wrong serach result") + } + + r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList) + if len(r5.Order) != 1 && r5.Order[0] != o1.Id { + t.Fatal("returned wrong serach result") + } + + r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList) + if len(r6.Order) != 1 && r6.Order[0] != o4.Id { + t.Fatal("returned wrong serach result") + } + + r7 := (<-store.Post().Search(teamId, userId, "#secret", true)).Data.(*model.PostList) + if len(r7.Order) != 1 && r7.Order[0] != o5.Id { + t.Fatal("returned wrong serach result") + } + + r8 := (<-store.Post().Search(teamId, userId, "@thisshouldmatchnothing", true)).Data.(*model.PostList) + if len(r8.Order) != 0 { + t.Fatal("returned wrong serach result") + } +} diff --git a/store/sql_session_store.go b/store/sql_session_store.go new file mode 100644 index 000000000..dddd023e5 --- /dev/null +++ b/store/sql_session_store.go @@ -0,0 +1,176 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + l4g "code.google.com/p/log4go" + "github.com/mattermost/platform/model" +) + +type SqlSessionStore struct { + *SqlStore +} + +func NewSqlSessionStore(sqlStore *SqlStore) SessionStore { + us := &SqlSessionStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Session{}, "Sessions").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("AltId").SetMaxSize(26) + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("TeamId").SetMaxSize(26) + table.ColMap("DeviceId").SetMaxSize(128) + table.ColMap("Roles").SetMaxSize(64) + table.ColMap("Props").SetMaxSize(1000) + } + + return us +} + +func (me SqlSessionStore) UpgradeSchemaIfNeeded() { +} + +func (me SqlSessionStore) CreateIndexesIfNotExists() { + me.CreateIndexIfNotExists("idx_user_id", "Sessions", "UserId") +} + +func (me SqlSessionStore) Save(session *model.Session) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if len(session.Id) > 0 { + result.Err = model.NewAppError("SqlSessionStore.Save", "Cannot update existing session", "id="+session.Id) + storeChannel <- result + close(storeChannel) + return + } + + session.PreSave() + + if cur := <-me.CleanUpExpiredSessions(session.UserId); cur.Err != nil { + l4g.Error("Failed to cleanup sessions in Save err=%v", cur.Err) + } + + if err := me.GetMaster().Insert(session); err != nil { + result.Err = model.NewAppError("SqlSessionStore.Save", "We couldn't save the session", "id="+session.Id+", "+err.Error()) + } else { + result.Data = session + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (me SqlSessionStore) Get(id string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if obj, err := me.GetReplica().Get(model.Session{}, id); err != nil { + result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "id="+id+", "+err.Error()) + } else if obj == nil { + result.Err = model.NewAppError("SqlSessionStore.Get", "We couldn't find the existing session", "id="+id) + } else { + result.Data = obj.(*model.Session) + } + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (me SqlSessionStore) GetSessions(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + + if cur := <-me.CleanUpExpiredSessions(userId); cur.Err != nil { + l4g.Error("Failed to cleanup sessions in getSessions err=%v", cur.Err) + } + + result := StoreResult{} + + var sessions []*model.Session + + if _, err := me.GetReplica().Select(&sessions, "SELECT * FROM Sessions WHERE UserId = ? ORDER BY LastActivityAt DESC", userId); err != nil { + result.Err = model.NewAppError("SqlSessionStore.GetSessions", "We encounted an error while finding user sessions", err.Error()) + } else { + + result.Data = sessions + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (me SqlSessionStore) Remove(sessionIdOrAlt string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE Id = ? Or AltId = ?", sessionIdOrAlt, sessionIdOrAlt) + if err != nil { + result.Err = model.NewAppError("SqlSessionStore.RemoveSession", "We couldn't remove the session", "id="+sessionIdOrAlt+", err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE UserId = ? AND ExpiresAt != 0 AND ? > ExpiresAt", userId, model.GetMillis()); err != nil { + result.Err = model.NewAppError("SqlSessionStore.CleanUpExpiredSessions", "We encounted an error while deleting expired user sessions", err.Error()) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (me SqlSessionStore) UpdateLastActivityAt(sessionId string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := me.GetMaster().Exec("UPDATE Sessions SET LastActivityAt = ? WHERE Id = ?", time, sessionId); err != nil { + result.Err = model.NewAppError("SqlSessionStore.UpdateLastActivityAt", "We couldn't update the last_activity_at", "sessionId="+sessionId) + } else { + result.Data = sessionId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go new file mode 100644 index 000000000..edb1d4c14 --- /dev/null +++ b/store/sql_session_store_test.go @@ -0,0 +1,134 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestSessionStoreSave(t *testing.T) { + Setup() + + s1 := model.Session{} + s1.UserId = model.NewId() + s1.TeamId = model.NewId() + + if err := (<-store.Session().Save(&s1)).Err; err != nil { + t.Fatal(err) + } +} + +func TestSessionGet(t *testing.T) { + Setup() + + s1 := model.Session{} + s1.UserId = model.NewId() + s1.TeamId = model.NewId() + <-store.Session().Save(&s1) + + s2 := model.Session{} + s2.UserId = s1.UserId + s2.TeamId = s1.TeamId + <-store.Session().Save(&s2) + + s3 := model.Session{} + s3.UserId = s1.UserId + s3.TeamId = s1.TeamId + s3.ExpiresAt = 1 + <-store.Session().Save(&s3) + + if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { + t.Fatal(rs1.Err) + } else { + if rs1.Data.(*model.Session).Id != s1.Id { + t.Fatal("should match") + } + } + + if rs2 := (<-store.Session().GetSessions(s1.UserId)); rs2.Err != nil { + t.Fatal(rs2.Err) + } else { + if len(rs2.Data.([]*model.Session)) != 2 { + t.Fatal("should match len") + } + } + +} + +func TestSessionRemove(t *testing.T) { + Setup() + + s1 := model.Session{} + s1.UserId = model.NewId() + s1.TeamId = model.NewId() + <-store.Session().Save(&s1) + + if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { + t.Fatal(rs1.Err) + } else { + if rs1.Data.(*model.Session).Id != s1.Id { + t.Fatal("should match") + } + } + + <-store.Session().Remove(s1.Id) + + if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil { + t.Fatal("should have been removed") + } +} + +func TestSessionRemoveAlt(t *testing.T) { + Setup() + + s1 := model.Session{} + s1.UserId = model.NewId() + s1.TeamId = model.NewId() + <-store.Session().Save(&s1) + + if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil { + t.Fatal(rs1.Err) + } else { + if rs1.Data.(*model.Session).Id != s1.Id { + t.Fatal("should match") + } + } + + <-store.Session().Remove(s1.AltId) + + if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil { + t.Fatal("should have been removed") + } + + if rs3 := (<-store.Session().GetSessions(s1.UserId)); rs3.Err != nil { + t.Fatal(rs3.Err) + } else { + if len(rs3.Data.([]*model.Session)) != 0 { + t.Fatal("should match len") + } + } +} + +func TestSessionStoreUpdateLastActivityAt(t *testing.T) { + Setup() + + s1 := model.Session{} + s1.UserId = model.NewId() + s1.TeamId = model.NewId() + <-store.Session().Save(&s1) + + if err := (<-store.Session().UpdateLastActivityAt(s1.Id, 1234567890)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.Session().Get(s1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.Session).LastActivityAt != 1234567890 { + t.Fatal("LastActivityAt not updated correctly") + } + } + +} diff --git a/store/sql_store.go b/store/sql_store.go new file mode 100644 index 000000000..a2deea6ba --- /dev/null +++ b/store/sql_store.go @@ -0,0 +1,372 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + l4g "code.google.com/p/log4go" + "crypto/aes" + "crypto/cipher" + crand "crypto/rand" + dbsql "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "github.com/go-gorp/gorp" + _ "github.com/go-sql-driver/mysql" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "io" + sqltrace "log" + "math/rand" + "os" + "time" +) + +type SqlStore struct { + master *gorp.DbMap + replicas []*gorp.DbMap + team TeamStore + channel ChannelStore + post PostStore + user UserStore + audit AuditStore + session SessionStore +} + +func NewSqlStore() Store { + + sqlStore := &SqlStore{} + + sqlStore.master = setupConnection("master", utils.Cfg.SqlSettings.DriverName, + utils.Cfg.SqlSettings.DataSource, utils.Cfg.SqlSettings.MaxIdleConns, + utils.Cfg.SqlSettings.MaxOpenConns, utils.Cfg.SqlSettings.Trace) + + sqlStore.replicas = make([]*gorp.DbMap, len(utils.Cfg.SqlSettings.DataSourceReplicas)) + for i, replica := range utils.Cfg.SqlSettings.DataSourceReplicas { + sqlStore.replicas[i] = setupConnection(fmt.Sprintf("replica-%v", i), utils.Cfg.SqlSettings.DriverName, replica, + utils.Cfg.SqlSettings.MaxIdleConns, utils.Cfg.SqlSettings.MaxOpenConns, + utils.Cfg.SqlSettings.Trace) + } + + sqlStore.team = NewSqlTeamStore(sqlStore) + sqlStore.channel = NewSqlChannelStore(sqlStore) + sqlStore.post = NewSqlPostStore(sqlStore) + sqlStore.user = NewSqlUserStore(sqlStore) + sqlStore.audit = NewSqlAuditStore(sqlStore) + sqlStore.session = NewSqlSessionStore(sqlStore) + + sqlStore.master.CreateTablesIfNotExists() + + sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() + sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() + sqlStore.post.(*SqlPostStore).CreateIndexesIfNotExists() + sqlStore.user.(*SqlUserStore).CreateIndexesIfNotExists() + sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists() + sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists() + + sqlStore.team.(*SqlTeamStore).UpgradeSchemaIfNeeded() + sqlStore.channel.(*SqlChannelStore).UpgradeSchemaIfNeeded() + sqlStore.post.(*SqlPostStore).UpgradeSchemaIfNeeded() + sqlStore.user.(*SqlUserStore).UpgradeSchemaIfNeeded() + sqlStore.audit.(*SqlAuditStore).UpgradeSchemaIfNeeded() + sqlStore.session.(*SqlSessionStore).UpgradeSchemaIfNeeded() + + return sqlStore +} + +func setupConnection(con_type string, driver string, dataSource string, maxIdle int, maxOpen int, trace bool) *gorp.DbMap { + + db, err := dbsql.Open(driver, dataSource) + if err != nil { + l4g.Critical("Failed to open sql connection to '%v' err:%v", dataSource, err) + time.Sleep(time.Second) + panic("Failed to open sql connection" + err.Error()) + } + + l4g.Info("Pinging sql %v database at '%v'", con_type, dataSource) + err = db.Ping() + if err != nil { + l4g.Critical("Failed to ping db err:%v", err) + time.Sleep(time.Second) + panic("Failed to open sql connection " + err.Error()) + } + + db.SetMaxIdleConns(maxIdle) + db.SetMaxOpenConns(maxOpen) + + var dbmap *gorp.DbMap + + if driver == "sqlite3" { + dbmap = &gorp.DbMap{Db: db, TypeConverter: mattermConverter{}, Dialect: gorp.SqliteDialect{}} + } else if driver == "mysql" { + dbmap = &gorp.DbMap{Db: db, TypeConverter: mattermConverter{}, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"}} + } else { + l4g.Critical("Failed to create dialect specific driver") + time.Sleep(time.Second) + panic("Failed to create dialect specific driver " + err.Error()) + } + + if trace { + dbmap.TraceOn("", sqltrace.New(os.Stdout, "sql-trace:", sqltrace.Lmicroseconds)) + } + + return dbmap +} + +func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string, afterName string, colType string, defaultValue string) bool { + count, err := ss.GetMaster().SelectInt( + `SELECT + COUNT(0) AS column_exists + FROM + information_schema.COLUMNS + WHERE + TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND COLUMN_NAME = ?`, + tableName, + columnName, + ) + if err != nil { + l4g.Critical("Failed to check if column exists %v", err) + time.Sleep(time.Second) + panic("Failed to check if column exists " + err.Error()) + } + + if count > 0 { + return false + } + + _, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " ADD " + columnName + " " + colType + " DEFAULT '" + defaultValue + "'" + " AFTER " + afterName) + if err != nil { + l4g.Critical("Failed to create column %v", err) + time.Sleep(time.Second) + panic("Failed to create column " + err.Error()) + } + + return true +} + +func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) bool { + count, err := ss.GetMaster().SelectInt( + `SELECT + COUNT(0) AS column_exists + FROM + information_schema.COLUMNS + WHERE + TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND COLUMN_NAME = ?`, + tableName, + columnName, + ) + if err != nil { + l4g.Critical("Failed to check if column exists %v", err) + time.Sleep(time.Second) + panic("Failed to check if column exists " + err.Error()) + } + + if count == 0 { + return false + } + + _, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " DROP COLUMN " + columnName) + if err != nil { + l4g.Critical("Failed to drop column %v", err) + time.Sleep(time.Second) + panic("Failed to drop column " + err.Error()) + } + + return true +} + +func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) { + ss.createIndexIfNotExists(indexName, tableName, columnName, false) +} + +func (ss SqlStore) CreateFullTextIndexIfNotExists(indexName string, tableName string, columnName string) { + ss.createIndexIfNotExists(indexName, tableName, columnName, true) +} + +func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, fullText bool) { + count, err := ss.GetMaster().SelectInt("SELECT COUNT(0) AS index_exists FROM information_schema.statistics WHERE TABLE_SCHEMA = DATABASE() and table_name = ? AND index_name = ?", tableName, indexName) + if err != nil { + l4g.Critical("Failed to check index", err) + time.Sleep(time.Second) + panic("Failed to check index" + err.Error()) + } + + if count > 0 { + return + } + + fullTextIndex := "" + if fullText { + fullTextIndex = " FULLTEXT " + } + + _, err = ss.GetMaster().Exec("CREATE " + fullTextIndex + " INDEX " + indexName + " ON " + tableName + " (" + columnName + ")") + if err != nil { + l4g.Critical("Failed to create index", err) + time.Sleep(time.Second) + panic("Failed to create index " + err.Error()) + } +} + +func (ss SqlStore) GetMaster() *gorp.DbMap { + return ss.master +} + +func (ss SqlStore) GetReplica() *gorp.DbMap { + return ss.replicas[rand.Intn(len(ss.replicas))] +} + +func (ss SqlStore) GetAllConns() []*gorp.DbMap { + all := make([]*gorp.DbMap, len(ss.replicas)+1) + copy(all, ss.replicas) + all[len(ss.replicas)] = ss.master + return all +} + +func (ss SqlStore) Close() { + l4g.Info("Closing SqlStore") + ss.master.Db.Close() + for _, replica := range ss.replicas { + replica.Db.Close() + } +} + +func (ss SqlStore) Team() TeamStore { + return ss.team +} + +func (ss SqlStore) Channel() ChannelStore { + return ss.channel +} + +func (ss SqlStore) Post() PostStore { + return ss.post +} + +func (ss SqlStore) User() UserStore { + return ss.user +} + +func (ss SqlStore) Session() SessionStore { + return ss.session +} + +func (ss SqlStore) Audit() AuditStore { + return ss.audit +} + +type mattermConverter struct{} + +func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { + + switch t := val.(type) { + case model.StringMap: + return model.MapToJson(t), nil + case model.StringArray: + return model.ArrayToJson(t), nil + case model.EncryptStringMap: + return encrypt([]byte(utils.Cfg.SqlSettings.AtRestEncryptKey), model.MapToJson(t)) + } + + return val, nil +} + +func (me mattermConverter) FromDb(target interface{}) (gorp.CustomScanner, bool) { + switch target.(type) { + case *model.StringMap: + binder := func(holder, target interface{}) error { + s, ok := holder.(*string) + if !ok { + return errors.New("FromDb: Unable to convert StringMap to *string") + } + b := []byte(*s) + return json.Unmarshal(b, target) + } + return gorp.CustomScanner{new(string), target, binder}, true + case *model.StringArray: + binder := func(holder, target interface{}) error { + s, ok := holder.(*string) + if !ok { + return errors.New("FromDb: Unable to convert StringArray to *string") + } + b := []byte(*s) + return json.Unmarshal(b, target) + } + return gorp.CustomScanner{new(string), target, binder}, true + case *model.EncryptStringMap: + binder := func(holder, target interface{}) error { + s, ok := holder.(*string) + if !ok { + return errors.New("FromDb: Unable to convert EncryptStringMap to *string") + } + + ue, err := decrypt([]byte(utils.Cfg.SqlSettings.AtRestEncryptKey), *s) + if err != nil { + return err + } + + b := []byte(ue) + return json.Unmarshal(b, target) + } + return gorp.CustomScanner{new(string), target, binder}, true + } + + return gorp.CustomScanner{}, false +} + +func encrypt(key []byte, text string) (string, error) { + + if text == "" || text == "{}" { + return "", nil + } + + plaintext := []byte(text) + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + ciphertext := make([]byte, aes.BlockSize+len(plaintext)) + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(crand.Reader, iv); err != nil { + return "", err + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) + + return base64.URLEncoding.EncodeToString(ciphertext), nil +} + +func decrypt(key []byte, cryptoText string) (string, error) { + + if cryptoText == "" || cryptoText == "{}" { + return "{}", nil + } + + ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText) + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + if len(ciphertext) < aes.BlockSize { + return "", errors.New("ciphertext too short") + } + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + stream := cipher.NewCFBDecrypter(block, iv) + + stream.XORKeyStream(ciphertext, ciphertext) + + return fmt.Sprintf("%s", ciphertext), nil +} diff --git a/store/sql_store_test.go b/store/sql_store_test.go new file mode 100644 index 000000000..84dbf5705 --- /dev/null +++ b/store/sql_store_test.go @@ -0,0 +1,83 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "strings" + "testing" +) + +var store Store + +func Setup() { + if store == nil { + utils.LoadConfig("config.json") + store = NewSqlStore() + } +} + +func TestSqlStore1(t *testing.T) { + utils.LoadConfig("config.json") + utils.Cfg.SqlSettings.Trace = true + + store := NewSqlStore() + store.Close() + + utils.LoadConfig("config.json") +} + +func TestSqlStore2(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("should have been fatal") + } + }() + + utils.LoadConfig("config.json") + utils.Cfg.SqlSettings.DriverName = "missing" + store = NewSqlStore() + + utils.LoadConfig("config.json") +} + +func TestSqlStore3(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("should have been fatal") + } + }() + + utils.LoadConfig("config.json") + utils.Cfg.SqlSettings.DataSource = "missing" + store = NewSqlStore() + + utils.LoadConfig("config.json") +} + +func TestEncrypt(t *testing.T) { + m := make(map[string]string) + + key := []byte("IPc17oYK9NAj6WfJeCqm5AxIBF6WBNuN") // AES-256 + + originalText1 := model.MapToJson(m) + cryptoText1, _ := encrypt(key, originalText1) + text1, _ := decrypt(key, cryptoText1) + rm1 := model.MapFromJson(strings.NewReader(text1)) + + if len(rm1) != 0 { + t.Fatal("error in encrypt") + } + + m["key"] = "value" + originalText2 := model.MapToJson(m) + cryptoText2, _ := encrypt(key, originalText2) + text2, _ := decrypt(key, cryptoText2) + rm2 := model.MapFromJson(strings.NewReader(text2)) + + if rm2["key"] != "value" { + t.Fatal("error in encrypt") + } +} diff --git a/store/sql_team_store.go b/store/sql_team_store.go new file mode 100644 index 000000000..6e7fc1c1e --- /dev/null +++ b/store/sql_team_store.go @@ -0,0 +1,195 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "strings" +) + +type SqlTeamStore struct { + *SqlStore +} + +func NewSqlTeamStore(sqlStore *SqlStore) TeamStore { + s := &SqlTeamStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Team{}, "Teams").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("Name").SetMaxSize(64) + table.ColMap("Domain").SetMaxSize(64).SetUnique(true) + table.ColMap("Email").SetMaxSize(128) + table.ColMap("CompanyName").SetMaxSize(64) + table.ColMap("AllowedDomains").SetMaxSize(500) + } + + return s +} + +func (s SqlTeamStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlTeamStore) CreateIndexesIfNotExists() { +} + +func (s SqlTeamStore) Save(team *model.Team) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if len(team.Id) > 0 { + result.Err = model.NewAppError("SqlTeamStore.Save", + "Must call update for exisiting team", "id="+team.Id) + storeChannel <- result + close(storeChannel) + return + } + + team.PreSave() + if result.Err = team.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(team); err != nil { + if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Domain'") { + result.Err = model.NewAppError("SqlTeamStore.Save", "A team with that domain already exists", "id="+team.Id+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlTeamStore.Save", "We couldn't save the team", "id="+team.Id+", "+err.Error()) + } + } else { + result.Data = team + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) Update(team *model.Team) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + team.PreUpdate() + + if result.Err = team.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if oldResult, err := s.GetMaster().Get(model.Team{}, team.Id); err != nil { + result.Err = model.NewAppError("SqlTeamStore.Update", "We encounted an error finding the team", "id="+team.Id+", "+err.Error()) + } else if oldResult == nil { + result.Err = model.NewAppError("SqlTeamStore.Update", "We couldn't find the existing team to update", "id="+team.Id) + } else { + oldTeam := oldResult.(*model.Team) + team.CreateAt = oldTeam.CreateAt + team.Domain = oldTeam.Domain + + if count, err := s.GetMaster().Update(team); err != nil { + result.Err = model.NewAppError("SqlTeamStore.Update", "We encounted an error updating the team", "id="+team.Id+", "+err.Error()) + } else if count != 1 { + result.Err = model.NewAppError("SqlTeamStore.Update", "We couldn't update the team", "id="+team.Id) + } else { + result.Data = team + } + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) UpdateName(name string, teamId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := s.GetMaster().Exec("UPDATE Teams SET Name = ? WHERE Id = ?", name, teamId); err != nil { + result.Err = model.NewAppError("SqlTeamStore.UpdateName", "We couldn't update the team name", "team_id="+teamId) + } else { + result.Data = teamId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) Get(id string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if obj, err := s.GetReplica().Get(model.Team{}, id); err != nil { + result.Err = model.NewAppError("SqlTeamStore.Get", "We encounted an error finding the team", "id="+id+", "+err.Error()) + } else if obj == nil { + result.Err = model.NewAppError("SqlTeamStore.Get", "We couldn't find the existing team", "id="+id) + } else { + result.Data = obj.(*model.Team) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) GetByDomain(domain string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + team := model.Team{} + + if err := s.GetReplica().SelectOne(&team, "SELECT * FROM Teams WHERE Domain=?", domain); err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetByDomain", "We couldn't find the existing team", "domain="+domain+", "+err.Error()) + } + + result.Data = &team + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var data []*model.Team + if _, err := s.GetReplica().Select(&data, "SELECT Teams.* FROM Teams, Users WHERE Teams.Id = Users.TeamId AND Users.Email = ?", email); err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetTeamsForEmail", "We encounted a problem when looking up teams", "email="+email+", "+err.Error()) + } + + result.Data = data + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go new file mode 100644 index 000000000..981817d90 --- /dev/null +++ b/store/sql_team_store_test.go @@ -0,0 +1,155 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestTeamStoreSave(t *testing.T) { + Setup() + + o1 := model.Team{} + o1.Name = "Name" + o1.Domain = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + + if err := (<-store.Team().Save(&o1)).Err; err != nil { + t.Fatal("couldn't save item", err) + } + + if err := (<-store.Team().Save(&o1)).Err; err == nil { + t.Fatal("shouldn't be able to update from save") + } + + o1.Id = "" + if err := (<-store.Team().Save(&o1)).Err; err == nil { + t.Fatal("should be unique domain") + } +} + +func TestTeamStoreUpdate(t *testing.T) { + Setup() + + o1 := model.Team{} + o1.Name = "Name" + o1.Domain = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + <-store.Team().Save(&o1) + + if err := (<-store.Team().Update(&o1)).Err; err != nil { + t.Fatal(err) + } + + o1.Id = "missing" + if err := (<-store.Team().Update(&o1)).Err; err == nil { + t.Fatal("Update should have failed because of missing key") + } + + o1.Id = model.NewId() + if err := (<-store.Team().Update(&o1)).Err; err == nil { + t.Fatal("Update should have faile because id change") + } +} + +func TestTeamStoreUpdateName(t *testing.T) { + Setup() + + o1 := &model.Team{} + o1.Name = "Name" + o1.Domain = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + o1 = (<-store.Team().Save(o1)).Data.(*model.Team) + + newName := "NewName" + + if err := (<-store.Team().UpdateName(newName, o1.Id)).Err; err != nil { + t.Fatal(err) + } + + ro1 := (<-store.Team().Get(o1.Id)).Data.(*model.Team) + if ro1.Name != newName { + t.Fatal("Name not updated") + } +} + +func TestTeamStoreGet(t *testing.T) { + Setup() + + o1 := model.Team{} + o1.Name = "Name" + o1.Domain = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + <-store.Team().Save(&o1) + + if r1 := <-store.Team().Get(o1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.Team).ToJson() != o1.ToJson() { + t.Fatal("invalid returned team") + } + } + + if err := (<-store.Team().Get("")).Err; err == nil { + t.Fatal("Missing id should have failed") + } +} + +func TestTeamStoreGetByDomain(t *testing.T) { + Setup() + + o1 := model.Team{} + o1.Name = "Name" + o1.Domain = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + <-store.Team().Save(&o1) + + if r1 := <-store.Team().GetByDomain(o1.Domain); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.Team).ToJson() != o1.ToJson() { + t.Fatal("invalid returned team") + } + } + + if err := (<-store.Team().GetByDomain("")).Err; err == nil { + t.Fatal("Missing id should have failed") + } +} + +func TestTeamStoreGetForEmail(t *testing.T) { + Setup() + + o1 := model.Team{} + o1.Name = "Name" + o1.Domain = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + <-store.Team().Save(&o1) + + u1 := model.User{} + u1.TeamId = o1.Id + u1.Email = model.NewId() + <-store.User().Save(&u1) + + if r1 := <-store.Team().GetTeamsForEmail(u1.Email); r1.Err != nil { + t.Fatal(r1.Err) + } else { + teams := r1.Data.([]*model.Team) + + if teams[0].Id != o1.Id { + t.Fatal("failed to lookup by email") + } + } + + if r1 := <-store.Team().GetTeamsForEmail("missing"); r1.Err != nil { + t.Fatal(r1.Err) + } +} diff --git a/store/sql_user_store.go b/store/sql_user_store.go new file mode 100644 index 000000000..abb8f2781 --- /dev/null +++ b/store/sql_user_store.go @@ -0,0 +1,367 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "fmt" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "strings" +) + +type SqlUserStore struct { + *SqlStore +} + +func NewSqlUserStore(sqlStore *SqlStore) UserStore { + us := &SqlUserStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.User{}, "Users").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("TeamId").SetMaxSize(26) + table.ColMap("Username").SetMaxSize(64) + table.ColMap("Password").SetMaxSize(128) + table.ColMap("AuthData").SetMaxSize(128) + table.ColMap("Email").SetMaxSize(128) + table.ColMap("FullName").SetMaxSize(64) + table.ColMap("Roles").SetMaxSize(64) + table.ColMap("Props").SetMaxSize(4000) + table.ColMap("NotifyProps").SetMaxSize(2000) + table.SetUniqueTogether("Email", "TeamId") + table.SetUniqueTogether("Username", "TeamId") + } + + return us +} + +func (s SqlUserStore) UpgradeSchemaIfNeeded() { +} + +func (us SqlUserStore) CreateIndexesIfNotExists() { + us.CreateIndexIfNotExists("idx_team_id", "Users", "TeamId") +} + +func (us SqlUserStore) Save(user *model.User) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if len(user.Id) > 0 { + result.Err = model.NewAppError("SqlUserStore.Save", "Must call update for exisiting user", "user_id="+user.Id) + storeChannel <- result + close(storeChannel) + return + } + + user.PreSave() + if result.Err = user.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if count, err := us.GetMaster().SelectInt("SELECT COUNT(0) FROM Users WHERE TeamId = ? AND DeleteAt = 0", user.TeamId); err != nil { + result.Err = model.NewAppError("SqlUserStore.Save", "Failed to get current team member count", "teamId="+user.TeamId+", "+err.Error()) + storeChannel <- result + close(storeChannel) + return + } else if int(count) > utils.Cfg.TeamSettings.MaxUsersPerTeam { + result.Err = model.NewAppError("SqlUserStore.Save", "You've reached the limit of the number of allowed accounts.", "teamId="+user.TeamId) + storeChannel <- result + close(storeChannel) + return + } + + if err := us.GetMaster().Insert(user); err != nil { + if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Email'") { + result.Err = model.NewAppError("SqlUserStore.Save", "An account with that email already exists.", "user_id="+user.Id+", "+err.Error()) + } else if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Username'") { + result.Err = model.NewAppError("SqlUserStore.Save", "An account with that username already exists.", "user_id="+user.Id+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlUserStore.Save", "We couldn't save the account.", "user_id="+user.Id+", "+err.Error()) + } + } else { + result.Data = user + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) Update(user *model.User, allowRoleActiveUpdate bool) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + user.PreUpdate() + + if result.Err = user.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if oldUserResult, err := us.GetMaster().Get(model.User{}, user.Id); err != nil { + result.Err = model.NewAppError("SqlUserStore.Update", "We encounted an error finding the account", "user_id="+user.Id+", "+err.Error()) + } else if oldUserResult == nil { + result.Err = model.NewAppError("SqlUserStore.Update", "We couldn't find the existing account to update", "user_id="+user.Id) + } else { + oldUser := oldUserResult.(*model.User) + user.CreateAt = oldUser.CreateAt + user.AuthData = oldUser.AuthData + user.Password = oldUser.Password + user.LastPasswordUpdate = oldUser.LastPasswordUpdate + user.TeamId = oldUser.TeamId + user.LastActivityAt = oldUser.LastActivityAt + user.LastPingAt = oldUser.LastPingAt + user.EmailVerified = oldUser.EmailVerified + + if !allowRoleActiveUpdate { + user.Roles = oldUser.Roles + user.DeleteAt = oldUser.DeleteAt + } + + if user.Email != oldUser.Email { + user.EmailVerified = false + } + + if count, err := us.GetMaster().Update(user); err != nil { + result.Err = model.NewAppError("SqlUserStore.Update", "We encounted an error updating the account", "user_id="+user.Id+", "+err.Error()) + } else if count != 1 { + result.Err = model.NewAppError("SqlUserStore.Update", "We couldn't update the account", "user_id="+user.Id) + } else { + result.Data = [2]*model.User{user, oldUser} + } + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) UpdateLastPingAt(userId string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := us.GetMaster().Exec("UPDATE Users SET LastPingAt = ? WHERE Id = ?", time, userId); err != nil { + result.Err = model.NewAppError("SqlUserStore.UpdateLastPingAt", "We couldn't update the last_ping_at", "user_id="+userId) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) UpdateLastActivityAt(userId string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := us.GetMaster().Exec("UPDATE Users SET LastActivityAt = ? WHERE Id = ?", time, userId); err != nil { + result.Err = model.NewAppError("SqlUserStore.UpdateLastActivityAt", "We couldn't update the last_activity_at", "user_id="+userId) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := us.GetMaster().Exec("UPDATE Sessions, Users SET Users.LastActivityAt = ?, Sessions.LastActivityAt = ? WHERE Users.Id = ? AND Sessions.Id = ?", time, time, userId, sessionId); err != nil { + result.Err = model.NewAppError("SqlUserStore.UpdateLastActivityAt", "We couldn't update the last_activity_at", "user_id="+userId+" session_id="+sessionId+" err="+err.Error()) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt := model.GetMillis() + + if _, err := us.GetMaster().Exec("UPDATE Users SET Password = ?, LastPasswordUpdate = ?, UpdateAt = ? WHERE Id = ?", hashedPassword, updateAt, updateAt, userId); err != nil { + result.Err = model.NewAppError("SqlUserStore.UpdatePassword", "We couldn't update the user password", "id="+userId+", "+err.Error()) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) Get(id string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if obj, err := us.GetReplica().Get(model.User{}, id); err != nil { + result.Err = model.NewAppError("SqlUserStore.Get", "We encounted an error finding the account", "user_id="+id+", "+err.Error()) + } else if obj == nil { + result.Err = model.NewAppError("SqlUserStore.Get", "We couldn't find the existing account", "user_id="+id) + } else { + result.Data = obj.(*model.User) + } + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (s SqlUserStore) GetEtagForProfiles(teamId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt, err := s.GetReplica().SelectInt("SELECT UpdateAt FROM Users WHERE TeamId = ? ORDER BY UpdateAt DESC LIMIT 1", teamId) + if err != nil { + result.Data = fmt.Sprintf("%v.%v", model.ETAG_ROOT_VERSION, model.GetMillis()) + } else { + result.Data = fmt.Sprintf("%v.%v", model.ETAG_ROOT_VERSION, updateAt) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) GetProfiles(teamId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var users []*model.User + + if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = ?", teamId); err != nil { + result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encounted an error while finding user profiles", err.Error()) + } else { + + userMap := make(map[string]*model.User) + + for _, u := range users { + u.Password = "" + u.AuthData = "" + userMap[u.Id] = u + } + + result.Data = userMap + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + user := model.User{} + + if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId=? AND Email=?", teamId, email); err != nil { + result.Err = model.NewAppError("SqlUserStore.GetByEmail", "We couldn't find the existing account", "teamId="+teamId+", email="+email+", "+err.Error()) + } + + result.Data = &user + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + user := model.User{} + + if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId=? AND Username=?", teamId, username); err != nil { + result.Err = model.NewAppError("SqlUserStore.GetByUsername", "We couldn't find the existing account", "teamId="+teamId+", username="+username+", "+err.Error()) + } + + result.Data = &user + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) VerifyEmail(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if _, err := us.GetMaster().Exec("UPDATE Users SET EmailVerified = 1 WHERE Id = ?", userId); err != nil { + result.Err = model.NewAppError("SqlUserStore.VerifyEmail", "Unable to update verify email field", "userId="+userId+", "+err.Error()) + } + + result.Data = userId + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go new file mode 100644 index 000000000..4231920a5 --- /dev/null +++ b/store/sql_user_store_test.go @@ -0,0 +1,276 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "strings" + "testing" +) + +func TestUserStoreSave(t *testing.T) { + Setup() + + u1 := model.User{} + u1.Email = model.NewId() + u1.Username = model.NewId() + u1.TeamId = model.NewId() + + if err := (<-store.User().Save(&u1)).Err; err != nil { + t.Fatal("couldn't save user", err) + } + + if err := (<-store.User().Save(&u1)).Err; err == nil { + t.Fatal("shouldn't be able to update user from save") + } + + u1.Id = "" + if err := (<-store.User().Save(&u1)).Err; err == nil { + t.Fatal("should be unique email") + } + + u1.Email = "" + if err := (<-store.User().Save(&u1)).Err; err == nil { + t.Fatal("should be unique username") + } + + u1.Email = strings.Repeat("0123456789", 20) + u1.Username = "" + if err := (<-store.User().Save(&u1)).Err; err == nil { + t.Fatal("should be unique username") + } + + for i := 0; i < 150; i++ { + u1.Id = "" + u1.Email = model.NewId() + u1.Username = model.NewId() + if err := (<-store.User().Save(&u1)).Err; err != nil { + t.Fatal("couldn't save item", err) + } + } + + u1.Id = "" + u1.Email = model.NewId() + u1.Username = model.NewId() + if err := (<-store.User().Save(&u1)).Err; err == nil { + t.Fatal("should be the limit", err) + } +} + +func TestUserStoreUpdate(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + if err := (<-store.User().Update(&u1, false)).Err; err != nil { + t.Fatal(err) + } + + u1.Id = "missing" + if err := (<-store.User().Update(&u1, false)).Err; err == nil { + t.Fatal("Update should have failed because of missing key") + } + + u1.Id = model.NewId() + if err := (<-store.User().Update(&u1, false)).Err; err == nil { + t.Fatal("Update should have faile because id change") + } +} + +func TestUserStoreUpdateLastPingAt(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + if err := (<-store.User().UpdateLastPingAt(u1.Id, 1234567890)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.User().Get(u1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.User).LastPingAt != 1234567890 { + t.Fatal("LastPingAt not updated correctly") + } + } + +} + +func TestUserStoreUpdateLastActivityAt(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + if err := (<-store.User().UpdateLastActivityAt(u1.Id, 1234567890)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.User().Get(u1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.User).LastActivityAt != 1234567890 { + t.Fatal("LastActivityAt not updated correctly") + } + } + +} + +func TestUserStoreUpdateUserAndSessionActivity(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + s1 := model.Session{} + s1.UserId = u1.Id + s1.TeamId = u1.TeamId + <-store.Session().Save(&s1) + + if err := (<-store.User().UpdateUserAndSessionActivity(u1.Id, s1.Id, 1234567890)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.User().Get(u1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.User).LastActivityAt != 1234567890 { + t.Fatal("LastActivityAt not updated correctly for user") + } + } + + if r2 := <-store.Session().Get(s1.Id); r2.Err != nil { + t.Fatal(r2.Err) + } else { + if r2.Data.(*model.Session).LastActivityAt != 1234567890 { + t.Fatal("LastActivityAt not updated correctly for session") + } + } + +} + +func TestUserStoreGet(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + if r1 := <-store.User().Get(u1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.User).ToJson() != u1.ToJson() { + t.Fatal("invalid returned user") + } + } + + if err := (<-store.User().Get("")).Err; err == nil { + t.Fatal("Missing id should have failed") + } +} + +func TestUserStoreGetProfiles(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + u2 := model.User{} + u2.TeamId = u1.TeamId + u2.Email = model.NewId() + <-store.User().Save(&u2) + + if r1 := <-store.User().GetProfiles(u1.TeamId); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.(map[string]*model.User) + if len(users) != 2 { + t.Fatal("invalid returned users") + } + + if users[u1.Id].Id != u1.Id { + t.Fatal("invalid returned user") + } + } + + if r2 := <-store.User().GetProfiles("123"); r2.Err != nil { + t.Fatal(r2.Err) + } else { + if len(r2.Data.(map[string]*model.User)) != 0 { + t.Fatal("should have returned empty map") + } + } +} + +func TestUserStoreGetByEmail(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + if err := (<-store.User().GetByEmail(u1.TeamId, u1.Email)).Err; err != nil { + t.Fatal(err) + } + + if err := (<-store.User().GetByEmail("", "")).Err; err == nil { + t.Fatal("Should have failed because of missing email") + } +} + +func TestUserStoreGetByUsername(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + u1.Username = model.NewId() + <-store.User().Save(&u1) + + if err := (<-store.User().GetByUsername(u1.TeamId, u1.Username)).Err; err != nil { + t.Fatal(err) + } + + if err := (<-store.User().GetByUsername("", "")).Err; err == nil { + t.Fatal("Should have failed because of missing username") + } +} + +func TestUserStoreUpdatePassword(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + <-store.User().Save(&u1) + + hashedPassword := model.HashPassword("newpwd") + + if err := (<-store.User().UpdatePassword(u1.Id, hashedPassword)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.User().GetByEmail(u1.TeamId, u1.Email); r1.Err != nil { + t.Fatal(r1.Err) + } else { + user := r1.Data.(*model.User) + if user.Password != hashedPassword { + t.Fatal("Password was not updated correctly") + } + } +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 000000000..8d4b49b6e --- /dev/null +++ b/store/store.go @@ -0,0 +1,94 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" +) + +type StoreResult struct { + Data interface{} + Err *model.AppError +} + +type StoreChannel chan StoreResult + +type Store interface { + Team() TeamStore + Channel() ChannelStore + Post() PostStore + User() UserStore + Audit() AuditStore + Session() SessionStore + Close() +} + +type TeamStore interface { + Save(team *model.Team) StoreChannel + Update(team *model.Team) StoreChannel + UpdateName(name string, teamId string) StoreChannel + Get(id string) StoreChannel + GetByDomain(domain string) StoreChannel + GetTeamsForEmail(domain string) StoreChannel +} + +type ChannelStore interface { + Save(channel *model.Channel) StoreChannel + Update(channel *model.Channel) StoreChannel + Get(id string) StoreChannel + Delete(channelId string, time int64) StoreChannel + GetByName(team_id string, domain string) StoreChannel + GetChannels(teamId string, userId string) StoreChannel + GetMoreChannels(teamId string, userId string) StoreChannel + + SaveMember(member *model.ChannelMember) StoreChannel + GetMembers(channelId string) StoreChannel + GetMember(channelId string, userId string) StoreChannel + RemoveMember(channelId string, userId string) StoreChannel + GetExtraMembers(channelId string, limit int) StoreChannel + CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel + CheckOpenChannelPermissions(teamId string, channelId string) StoreChannel + CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel + UpdateLastViewedAt(channelId string, userId string) StoreChannel + IncrementMentionCount(channelId string, userId string) StoreChannel + UpdateNotifyLevel(channelId string, userId string, notifyLevel string) StoreChannel +} + +type PostStore interface { + Save(post *model.Post) StoreChannel + Update(post *model.Post, newMessage string, newHashtags string) StoreChannel + Get(id string) StoreChannel + Delete(postId string, time int64) StoreChannel + GetPosts(channelId string, offset int, limit int) StoreChannel + GetEtag(channelId string) StoreChannel + Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel +} + +type UserStore interface { + Save(user *model.User) StoreChannel + Update(user *model.User, allowRoleUpdate bool) StoreChannel + UpdateLastPingAt(userId string, time int64) StoreChannel + UpdateLastActivityAt(userId string, time int64) StoreChannel + UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel + UpdatePassword(userId, newPassword string) StoreChannel + Get(id string) StoreChannel + GetProfiles(teamId string) StoreChannel + GetByEmail(teamId string, email string) StoreChannel + GetByUsername(teamId string, username string) StoreChannel + VerifyEmail(userId string) StoreChannel + GetEtagForProfiles(teamId string) StoreChannel +} + +type SessionStore interface { + Save(session *model.Session) StoreChannel + Get(id string) StoreChannel + GetSessions(userId string) StoreChannel + Remove(sessionIdOrAlt string) StoreChannel + UpdateLastActivityAt(sessionId string, time int64) StoreChannel +} + +type AuditStore interface { + Save(audit *model.Audit) StoreChannel + Get(user_id string, limit int) StoreChannel +} -- cgit v1.2.3-1-g7c22