3ae821d2e8
* Refactor websockets state and lifecycle This PR moves the state of the authentication and subscriptions to the websockets client, allowing for multiple components to communicate with it and request subscriptions independently. With this change, the lifecycle of the websockets client is now managed on a component, and a hook is provided for easy access to it from individual components. * Fix linter * Integrating the new websockets in channels integration with the RHS and board selector * Some small fixes around boards-channels relationship * Make the boards unfurl to always use the current team * Fixing weird behaviors in websockets and other small data related bugs in channel-board relationship * Only warn if withWebSockets is used without a base connection * Fix tests * Fix linter * Update snapshot * Fixing plugin tests Co-authored-by: Jesús Espino <jespinog@gmail.com>
879 lines
23 KiB
Go
879 lines
23 KiB
Go
package sqlstore
|
|
|
|
import (
|
|
//nolint:gosec
|
|
"crypto/md5"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattermost/focalboard/server/utils"
|
|
|
|
sq "github.com/Masterminds/squirrel"
|
|
"github.com/mattermost/focalboard/server/model"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
)
|
|
|
|
type BoardNotFoundErr struct {
|
|
boardID string
|
|
}
|
|
|
|
func (be BoardNotFoundErr) Error() string {
|
|
return fmt.Sprintf("board not found (board id: %s", be.boardID)
|
|
}
|
|
|
|
func boardFields(prefix string) []string {
|
|
fields := []string{
|
|
"id",
|
|
"team_id",
|
|
"COALESCE(channel_id, '')",
|
|
"COALESCE(created_by, '')",
|
|
"modified_by",
|
|
"type",
|
|
"minimum_role",
|
|
"title",
|
|
"description",
|
|
"icon",
|
|
"show_description",
|
|
"is_template",
|
|
"template_version",
|
|
"COALESCE(properties, '{}')",
|
|
"COALESCE(card_properties, '[]')",
|
|
"create_at",
|
|
"update_at",
|
|
"delete_at",
|
|
}
|
|
|
|
if prefix == "" {
|
|
return fields
|
|
}
|
|
|
|
prefixedFields := make([]string, len(fields))
|
|
for i, field := range fields {
|
|
if strings.HasPrefix(field, "COALESCE(") {
|
|
prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1)
|
|
} else {
|
|
prefixedFields[i] = prefix + field
|
|
}
|
|
}
|
|
return prefixedFields
|
|
}
|
|
|
|
func boardHistoryFields() []string {
|
|
fields := []string{
|
|
"id",
|
|
"team_id",
|
|
"COALESCE(channel_id, '')",
|
|
"COALESCE(created_by, '')",
|
|
"COALESCE(modified_by, '')",
|
|
"type",
|
|
"minimum_role",
|
|
"COALESCE(title, '')",
|
|
"COALESCE(description, '')",
|
|
"COALESCE(icon, '')",
|
|
"COALESCE(show_description, false)",
|
|
"COALESCE(is_template, false)",
|
|
"template_version",
|
|
"COALESCE(properties, '{}')",
|
|
"COALESCE(card_properties, '[]')",
|
|
"COALESCE(create_at, 0)",
|
|
"COALESCE(update_at, 0)",
|
|
"COALESCE(delete_at, 0)",
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
var boardMemberFields = []string{
|
|
"COALESCE(B.minimum_role, '')",
|
|
"BM.board_id",
|
|
"BM.user_id",
|
|
"BM.roles",
|
|
"BM.scheme_admin",
|
|
"BM.scheme_editor",
|
|
"BM.scheme_commenter",
|
|
"BM.scheme_viewer",
|
|
}
|
|
|
|
func (s *SQLStore) boardsFromRows(rows *sql.Rows) ([]*model.Board, error) {
|
|
boards := []*model.Board{}
|
|
|
|
for rows.Next() {
|
|
var board model.Board
|
|
var propertiesBytes []byte
|
|
var cardPropertiesBytes []byte
|
|
|
|
err := rows.Scan(
|
|
&board.ID,
|
|
&board.TeamID,
|
|
&board.ChannelID,
|
|
&board.CreatedBy,
|
|
&board.ModifiedBy,
|
|
&board.Type,
|
|
&board.MinimumRole,
|
|
&board.Title,
|
|
&board.Description,
|
|
&board.Icon,
|
|
&board.ShowDescription,
|
|
&board.IsTemplate,
|
|
&board.TemplateVersion,
|
|
&propertiesBytes,
|
|
&cardPropertiesBytes,
|
|
&board.CreateAt,
|
|
&board.UpdateAt,
|
|
&board.DeleteAt,
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("boardsFromRows scan error", mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
|
|
err = json.Unmarshal(propertiesBytes, &board.Properties)
|
|
if err != nil {
|
|
s.logger.Error("board properties unmarshal error", mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(cardPropertiesBytes, &board.CardProperties)
|
|
if err != nil {
|
|
s.logger.Error("board card properties unmarshal error", mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
|
|
boards = append(boards, &board)
|
|
}
|
|
|
|
return boards, nil
|
|
}
|
|
|
|
func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, error) {
|
|
boardMembers := []*model.BoardMember{}
|
|
|
|
for rows.Next() {
|
|
var boardMember model.BoardMember
|
|
|
|
err := rows.Scan(
|
|
&boardMember.MinimumRole,
|
|
&boardMember.BoardID,
|
|
&boardMember.UserID,
|
|
&boardMember.Roles,
|
|
&boardMember.SchemeAdmin,
|
|
&boardMember.SchemeEditor,
|
|
&boardMember.SchemeCommenter,
|
|
&boardMember.SchemeViewer,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
boardMembers = append(boardMembers, &boardMember)
|
|
}
|
|
|
|
return boardMembers, nil
|
|
}
|
|
|
|
func (s *SQLStore) boardMemberHistoryEntriesFromRows(rows *sql.Rows) ([]*model.BoardMemberHistoryEntry, error) {
|
|
boardMemberHistoryEntries := []*model.BoardMemberHistoryEntry{}
|
|
|
|
for rows.Next() {
|
|
var boardMemberHistoryEntry model.BoardMemberHistoryEntry
|
|
var insertAt sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&boardMemberHistoryEntry.BoardID,
|
|
&boardMemberHistoryEntry.UserID,
|
|
&boardMemberHistoryEntry.Action,
|
|
&insertAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// parse the insert_at timestamp which is different based on database type.
|
|
dateTemplate := "2006-01-02T15:04:05Z0700"
|
|
if s.dbType == model.MysqlDBType {
|
|
dateTemplate = "2006-01-02 15:04:05.000000"
|
|
}
|
|
ts, err := time.Parse(dateTemplate, insertAt.String)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse datetime '%s' for board_members_history scan: %w", insertAt.String, err)
|
|
}
|
|
boardMemberHistoryEntry.InsertAt = ts
|
|
|
|
boardMemberHistoryEntries = append(boardMemberHistoryEntries, &boardMemberHistoryEntry)
|
|
}
|
|
|
|
return boardMemberHistoryEntries, nil
|
|
}
|
|
|
|
func (s *SQLStore) getBoardByCondition(db sq.BaseRunner, conditions ...interface{}) (*model.Board, error) {
|
|
boards, err := s.getBoardsByCondition(db, conditions...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return boards[0], nil
|
|
}
|
|
|
|
func (s *SQLStore) getBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) {
|
|
return s.getBoardsFieldsByCondition(db, boardFields(""), conditions...)
|
|
}
|
|
|
|
func (s *SQLStore) getBoardsFieldsByCondition(db sq.BaseRunner, fields []string, conditions ...interface{}) ([]*model.Board, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(fields...).
|
|
From(s.tablePrefix + "boards")
|
|
for _, c := range conditions {
|
|
query = query.Where(c)
|
|
}
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getBoardsByCondition ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
boards, err := s.boardsFromRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(boards) == 0 {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
|
|
return boards, nil
|
|
}
|
|
|
|
func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, error) {
|
|
return s.getBoardByCondition(db, sq.Eq{"id": boardID})
|
|
}
|
|
|
|
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardFields("b.")...).
|
|
Distinct().
|
|
From(s.tablePrefix + "boards as b").
|
|
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
|
Where(sq.Eq{"b.team_id": teamID}).
|
|
Where(sq.Eq{"b.is_template": false}).
|
|
Where(sq.Or{
|
|
sq.Eq{"b.type": model.BoardTypeOpen},
|
|
sq.And{
|
|
sq.Eq{"b.type": model.BoardTypePrivate},
|
|
sq.Eq{"bm.user_id": userID},
|
|
},
|
|
})
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getBoardsForUserAndTeam ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
return s.boardsFromRows(rows)
|
|
}
|
|
|
|
func (s *SQLStore) getBoardsInTeamByIds(db sq.BaseRunner, boardIDs []string, teamID string) ([]*model.Board, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardFields("b.")...).
|
|
From(s.tablePrefix + "boards as b").
|
|
Where(sq.Eq{"b.team_id": teamID}).
|
|
Where(sq.Eq{"b.is_template": false}).
|
|
Where(sq.Eq{"b.id": boardIDs})
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getBoardsInTeamByIds ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
return s.boardsFromRows(rows)
|
|
}
|
|
|
|
func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) {
|
|
// Generate tracking IDs for in-built templates
|
|
if board.IsTemplate && board.TeamID == model.GlobalTeamID {
|
|
//nolint:gosec
|
|
// we don't need cryptographically secure hash, so MD5 is fine
|
|
board.Properties["trackingTemplateId"] = fmt.Sprintf("%x", md5.Sum([]byte(board.Title)))
|
|
}
|
|
|
|
propertiesBytes, err := s.MarshalJSONB(board.Properties)
|
|
if err != nil {
|
|
s.logger.Error(
|
|
"failed to marshal board.Properties",
|
|
mlog.String("board_id", board.ID),
|
|
mlog.String("board.Properties", fmt.Sprintf("%v", board.Properties)),
|
|
mlog.Err(err),
|
|
)
|
|
return nil, err
|
|
}
|
|
|
|
cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties)
|
|
if err != nil {
|
|
s.logger.Error(
|
|
"failed to marshal board.CardProperties",
|
|
mlog.String("board_id", board.ID),
|
|
mlog.String("board.CardProperties", fmt.Sprintf("%v", board.CardProperties)),
|
|
mlog.Err(err),
|
|
)
|
|
return nil, err
|
|
}
|
|
|
|
existingBoard, err := s.getBoard(db, board.ID)
|
|
if err != nil && !model.IsErrNotFound(err) {
|
|
return nil, fmt.Errorf("insertBoard error occurred while fetching existing board %s: %w", board.ID, err)
|
|
}
|
|
|
|
insertQuery := s.getQueryBuilder(db).Insert("").
|
|
Columns(boardFields("")...)
|
|
|
|
now := utils.GetMillis()
|
|
|
|
insertQueryValues := map[string]interface{}{
|
|
"id": board.ID,
|
|
"team_id": board.TeamID,
|
|
"channel_id": board.ChannelID,
|
|
"created_by": board.CreatedBy,
|
|
"modified_by": userID,
|
|
"type": board.Type,
|
|
"title": board.Title,
|
|
"minimum_role": board.MinimumRole,
|
|
"description": board.Description,
|
|
"icon": board.Icon,
|
|
"show_description": board.ShowDescription,
|
|
"is_template": board.IsTemplate,
|
|
"template_version": board.TemplateVersion,
|
|
"properties": propertiesBytes,
|
|
"card_properties": cardPropertiesBytes,
|
|
"create_at": board.CreateAt,
|
|
"update_at": now,
|
|
"delete_at": board.DeleteAt,
|
|
}
|
|
|
|
if existingBoard != nil {
|
|
query := s.getQueryBuilder(db).Update(s.tablePrefix+"boards").
|
|
Where(sq.Eq{"id": board.ID}).
|
|
Set("modified_by", userID).
|
|
Set("type", board.Type).
|
|
Set("channel_id", board.ChannelID).
|
|
Set("minimum_role", board.MinimumRole).
|
|
Set("title", board.Title).
|
|
Set("description", board.Description).
|
|
Set("icon", board.Icon).
|
|
Set("show_description", board.ShowDescription).
|
|
Set("is_template", board.IsTemplate).
|
|
Set("template_version", board.TemplateVersion).
|
|
Set("properties", propertiesBytes).
|
|
Set("card_properties", cardPropertiesBytes).
|
|
Set("update_at", now).
|
|
Set("delete_at", board.DeleteAt)
|
|
|
|
if _, err := query.Exec(); err != nil {
|
|
s.logger.Error(`InsertBoard error occurred while updating existing board`, mlog.String("boardID", board.ID), mlog.Err(err))
|
|
return nil, fmt.Errorf("insertBoard error occurred while updating existing board %s: %w", board.ID, err)
|
|
}
|
|
} else {
|
|
insertQueryValues["created_by"] = userID
|
|
insertQueryValues["create_at"] = now
|
|
insertQueryValues["update_at"] = now
|
|
|
|
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards")
|
|
if _, err := query.Exec(); err != nil {
|
|
return nil, fmt.Errorf("insertBoard error occurred while inserting board %s: %w", board.ID, err)
|
|
}
|
|
}
|
|
|
|
// writing board history
|
|
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history")
|
|
if _, err := query.Exec(); err != nil {
|
|
s.logger.Error("failed to insert board history", mlog.String("board_id", board.ID), mlog.Err(err))
|
|
return nil, fmt.Errorf("failed to insert board %s history: %w", board.ID, err)
|
|
}
|
|
|
|
return s.getBoard(db, board.ID)
|
|
}
|
|
|
|
func (s *SQLStore) patchBoard(db sq.BaseRunner, boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) {
|
|
existingBoard, err := s.getBoard(db, boardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existingBoard == nil {
|
|
return nil, BoardNotFoundErr{boardID}
|
|
}
|
|
|
|
board := boardPatch.Patch(existingBoard)
|
|
return s.insertBoard(db, board, userID)
|
|
}
|
|
|
|
func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error {
|
|
now := utils.GetMillis()
|
|
|
|
board, err := s.getBoard(db, boardID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
propertiesBytes, err := s.MarshalJSONB(board.Properties)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
insertQueryValues := map[string]interface{}{
|
|
"id": board.ID,
|
|
"team_id": board.TeamID,
|
|
"channel_id": board.ChannelID,
|
|
"created_by": board.CreatedBy,
|
|
"modified_by": userID,
|
|
"type": board.Type,
|
|
"minimum_role": board.MinimumRole,
|
|
"title": board.Title,
|
|
"description": board.Description,
|
|
"icon": board.Icon,
|
|
"show_description": board.ShowDescription,
|
|
"is_template": board.IsTemplate,
|
|
"template_version": board.TemplateVersion,
|
|
"properties": propertiesBytes,
|
|
"card_properties": cardPropertiesBytes,
|
|
"create_at": board.CreateAt,
|
|
"update_at": now,
|
|
"delete_at": now,
|
|
}
|
|
|
|
// writing board history
|
|
insertQuery := s.getQueryBuilder(db).Insert("").
|
|
Columns(boardHistoryFields()...)
|
|
|
|
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history")
|
|
if _, err := query.Exec(); err != nil {
|
|
return err
|
|
}
|
|
|
|
deleteQuery := s.getQueryBuilder(db).
|
|
Delete(s.tablePrefix + "boards").
|
|
Where(sq.Eq{"id": boardID}).
|
|
Where(sq.Eq{"COALESCE(team_id, '0')": board.TeamID})
|
|
|
|
if _, err := deleteQuery.Exec(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLStore) insertBoardWithAdmin(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, *model.BoardMember, error) {
|
|
newBoard, err := s.insertBoard(db, board, userID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
bm := &model.BoardMember{
|
|
BoardID: newBoard.ID,
|
|
UserID: newBoard.CreatedBy,
|
|
SchemeAdmin: true,
|
|
SchemeEditor: true,
|
|
}
|
|
|
|
nbm, err := s.saveMember(db, bm)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot save member %s while inserting board %s: %w", bm.UserID, bm.BoardID, err)
|
|
}
|
|
|
|
return newBoard, nbm, nil
|
|
}
|
|
|
|
func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.BoardMember, error) {
|
|
queryValues := map[string]interface{}{
|
|
"board_id": bm.BoardID,
|
|
"user_id": bm.UserID,
|
|
"roles": "",
|
|
"scheme_admin": bm.SchemeAdmin,
|
|
"scheme_editor": bm.SchemeEditor,
|
|
"scheme_commenter": bm.SchemeCommenter,
|
|
"scheme_viewer": bm.SchemeViewer,
|
|
}
|
|
|
|
oldMember, err := s.getMemberForBoard(db, bm.BoardID, bm.UserID)
|
|
if err != nil && !model.IsErrNotFound(err) {
|
|
return nil, err
|
|
}
|
|
|
|
query := s.getQueryBuilder(db).
|
|
Insert(s.tablePrefix + "board_members").
|
|
SetMap(queryValues)
|
|
|
|
if s.dbType == model.MysqlDBType {
|
|
query = query.Suffix(
|
|
"ON DUPLICATE KEY UPDATE scheme_admin = ?, scheme_editor = ?, scheme_commenter = ?, scheme_viewer = ?",
|
|
bm.SchemeAdmin, bm.SchemeEditor, bm.SchemeCommenter, bm.SchemeViewer)
|
|
} else {
|
|
query = query.Suffix(
|
|
`ON CONFLICT (board_id, user_id)
|
|
DO UPDATE SET scheme_admin = EXCLUDED.scheme_admin, scheme_editor = EXCLUDED.scheme_editor,
|
|
scheme_commenter = EXCLUDED.scheme_commenter, scheme_viewer = EXCLUDED.scheme_viewer`,
|
|
)
|
|
}
|
|
|
|
if _, err := query.Exec(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if oldMember == nil {
|
|
addToMembersHistory := s.getQueryBuilder(db).
|
|
Insert(s.tablePrefix+"board_members_history").
|
|
Columns("board_id", "user_id", "action").
|
|
Values(bm.BoardID, bm.UserID, "created")
|
|
|
|
if _, err := addToMembersHistory.Exec(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return bm, nil
|
|
}
|
|
|
|
func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error {
|
|
deleteQuery := s.getQueryBuilder(db).
|
|
Delete(s.tablePrefix + "board_members").
|
|
Where(sq.Eq{"board_id": boardID}).
|
|
Where(sq.Eq{"user_id": userID})
|
|
|
|
result, err := deleteQuery.Exec()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rowsAffected > 0 {
|
|
addToMembersHistory := s.getQueryBuilder(db).
|
|
Insert(s.tablePrefix+"board_members_history").
|
|
Columns("board_id", "user_id", "action").
|
|
Values(boardID, userID, "deleted")
|
|
|
|
if _, err := addToMembersHistory.Exec(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (*model.BoardMember, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardMemberFields...).
|
|
From(s.tablePrefix + "board_members AS BM").
|
|
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
|
|
Where(sq.Eq{"BM.board_id": boardID}).
|
|
Where(sq.Eq{"BM.user_id": userID})
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getMemberForBoard ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
members, err := s.boardMembersFromRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
|
|
return members[0], nil
|
|
}
|
|
|
|
func (s *SQLStore) getMembersForUser(db sq.BaseRunner, userID string) ([]*model.BoardMember, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardMemberFields...).
|
|
From(s.tablePrefix + "board_members AS BM").
|
|
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
|
|
Where(sq.Eq{"BM.user_id": userID})
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
members, err := s.boardMembersFromRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*model.BoardMember, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardMemberFields...).
|
|
From(s.tablePrefix + "board_members AS BM").
|
|
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
|
|
Where(sq.Eq{"BM.board_id": boardID})
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
return s.boardMembersFromRows(rows)
|
|
}
|
|
|
|
// searchBoardsForUser returns all boards that match with the
|
|
// term that are either private and which the user is a member of, or
|
|
// they're open, regardless of the user membership.
|
|
// Search is case-insensitive.
|
|
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term, userID string) ([]*model.Board, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardFields("b.")...).
|
|
Distinct().
|
|
From(s.tablePrefix + "boards as b").
|
|
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
|
Where(sq.Eq{"b.is_template": false}).
|
|
Where(sq.Or{
|
|
sq.Eq{"b.type": model.BoardTypeOpen},
|
|
sq.And{
|
|
sq.Eq{"b.type": model.BoardTypePrivate},
|
|
sq.Eq{"bm.user_id": userID},
|
|
},
|
|
})
|
|
|
|
if term != "" {
|
|
// break search query into space separated words
|
|
// and search for each word.
|
|
// This should later be upgraded to industrial-strength
|
|
// word tokenizer, that uses much more than space
|
|
// to break words.
|
|
|
|
conditions := sq.Or{}
|
|
|
|
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
|
|
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
|
|
}
|
|
|
|
query = query.Where(conditions)
|
|
}
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
return s.boardsFromRows(rows)
|
|
}
|
|
|
|
// searchBoardsForUserInTeam returns all boards that match with the
|
|
// term that are either private and which the user is a member of, or
|
|
// they're open, regardless of the user membership.
|
|
// Search is case-insensitive.
|
|
func (s *SQLStore) searchBoardsForUserInTeam(db sq.BaseRunner, teamID, term, userID string) ([]*model.Board, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardFields("b.")...).
|
|
Distinct().
|
|
From(s.tablePrefix + "boards as b").
|
|
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
|
Where(sq.Eq{"b.is_template": false}).
|
|
Where(sq.Eq{"b.team_id": teamID}).
|
|
Where(sq.Or{
|
|
sq.Eq{"b.type": model.BoardTypeOpen},
|
|
sq.And{
|
|
sq.Eq{"b.type": model.BoardTypePrivate},
|
|
sq.Eq{"bm.user_id": userID},
|
|
},
|
|
})
|
|
|
|
if term != "" {
|
|
// break search query into space separated words
|
|
// and search for each word.
|
|
// This should later be upgraded to industrial-strength
|
|
// word tokenizer, that uses much more than space
|
|
// to break words.
|
|
|
|
conditions := sq.Or{}
|
|
|
|
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
|
|
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
|
|
}
|
|
|
|
query = query.Where(conditions)
|
|
}
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
return s.boardsFromRows(rows)
|
|
}
|
|
|
|
func (s *SQLStore) getBoardHistory(db sq.BaseRunner, boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) {
|
|
var order string
|
|
if opts.Descending {
|
|
order = " DESC "
|
|
}
|
|
|
|
query := s.getQueryBuilder(db).
|
|
Select(boardHistoryFields()...).
|
|
From(s.tablePrefix + "boards_history").
|
|
Where(sq.Eq{"id": boardID}).
|
|
OrderBy("insert_at " + order + ", update_at" + order)
|
|
|
|
if opts.BeforeUpdateAt != 0 {
|
|
query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt})
|
|
}
|
|
|
|
if opts.AfterUpdateAt != 0 {
|
|
query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt})
|
|
}
|
|
|
|
if opts.Limit != 0 {
|
|
query = query.Limit(opts.Limit)
|
|
}
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getBoardHistory ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
return s.boardsFromRows(rows)
|
|
}
|
|
|
|
func (s *SQLStore) undeleteBoard(db sq.BaseRunner, boardID string, modifiedBy string) error {
|
|
boards, err := s.getBoardHistory(db, boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(boards) == 0 {
|
|
s.logger.Warn("undeleteBlock board not found", mlog.String("board_id", boardID))
|
|
return nil // undeleting non-existing board is not considered an error (for now)
|
|
}
|
|
board := boards[0]
|
|
|
|
if board.DeleteAt == 0 {
|
|
s.logger.Warn("undeleteBlock board not deleted", mlog.String("board_id", board.ID))
|
|
return nil // undeleting not deleted board is not considered an error (for now)
|
|
}
|
|
|
|
propertiesJSON, err := s.MarshalJSONB(board.Properties)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cardPropertiesJSON, err := s.MarshalJSONB(board.CardProperties)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
now := utils.GetMillis()
|
|
columns := []string{
|
|
"id",
|
|
"team_id",
|
|
"channel_id",
|
|
"created_by",
|
|
"modified_by",
|
|
"type",
|
|
"title",
|
|
"minimum_role",
|
|
"description",
|
|
"icon",
|
|
"show_description",
|
|
"is_template",
|
|
"template_version",
|
|
"properties",
|
|
"card_properties",
|
|
"create_at",
|
|
"update_at",
|
|
"delete_at",
|
|
}
|
|
|
|
values := []interface{}{
|
|
board.ID,
|
|
board.TeamID,
|
|
"",
|
|
board.CreatedBy,
|
|
modifiedBy,
|
|
board.Type,
|
|
board.Title,
|
|
board.MinimumRole,
|
|
board.Description,
|
|
board.Icon,
|
|
board.ShowDescription,
|
|
board.IsTemplate,
|
|
board.TemplateVersion,
|
|
propertiesJSON,
|
|
cardPropertiesJSON,
|
|
board.CreateAt,
|
|
now,
|
|
0,
|
|
}
|
|
insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards_history").
|
|
Columns(columns...).
|
|
Values(values...)
|
|
insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards").
|
|
Columns(columns...).
|
|
Values(values...)
|
|
|
|
if _, err := insertHistoryQuery.Exec(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := insertQuery.Exec(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLStore) getBoardMemberHistory(db sq.BaseRunner, boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
|
|
query := s.getQueryBuilder(db).
|
|
Select("board_id", "user_id", "action", "insert_at").
|
|
From(s.tablePrefix + "board_members_history").
|
|
Where(sq.Eq{"board_id": boardID}).
|
|
Where(sq.Eq{"user_id": userID}).
|
|
OrderBy("insert_at DESC")
|
|
|
|
if limit > 0 {
|
|
query = query.Limit(limit)
|
|
}
|
|
|
|
rows, err := query.Query()
|
|
if err != nil {
|
|
s.logger.Error(`getBoardMemberHistory ERROR`, mlog.Err(err))
|
|
return nil, err
|
|
}
|
|
defer s.CloseRows(rows)
|
|
|
|
memberHistory, err := s.boardMemberHistoryEntriesFromRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return memberHistory, nil
|
|
}
|