You've already forked focalboard
mirror of
https://github.com/mattermost/focalboard.git
synced 2025-07-15 23:54:29 +02:00
Adds limits implementation to the server (#3213)
* Adds limits implementation to the server * Add test for deleted boards on active card count
This commit is contained in:
committed by
GitHub
parent
d34bf0391b
commit
fa6de94070
@ -17,6 +17,7 @@ require (
|
|||||||
github.com/Masterminds/squirrel v1.5.2 // indirect
|
github.com/Masterminds/squirrel v1.5.2 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||||
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
@ -210,6 +210,7 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
|
|||||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
github.com/blevesearch/bleve/v2 v2.3.2/go.mod h1:96+xE5pZUOsr3Y4vHzV1cBC837xZCpwLlX0hrrxnvIg=
|
github.com/blevesearch/bleve/v2 v2.3.2/go.mod h1:96+xE5pZUOsr3Y4vHzV1cBC837xZCpwLlX0hrrxnvIg=
|
||||||
github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/mattermost/focalboard/server/services/store"
|
"github.com/mattermost/focalboard/server/services/store"
|
||||||
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
|
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
|
||||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||||
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
"github.com/mattermost/focalboard/server/ws"
|
"github.com/mattermost/focalboard/server/ws"
|
||||||
|
|
||||||
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
||||||
@ -152,6 +153,8 @@ func (p *Plugin) OnActivate() error {
|
|||||||
WSAdapter: p.wsPluginAdapter,
|
WSAdapter: p.wsPluginAdapter,
|
||||||
NotifyBackends: notifyBackends,
|
NotifyBackends: notifyBackends,
|
||||||
PermissionsService: permissionsService,
|
PermissionsService: permissionsService,
|
||||||
|
PluginAPI: p.API,
|
||||||
|
Client: client,
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := server.New(params)
|
server, err := server.New(params)
|
||||||
@ -162,6 +165,19 @@ func (p *Plugin) OnActivate() error {
|
|||||||
|
|
||||||
backendParams.appAPI.init(db, server.App())
|
backendParams.appAPI.init(db, server.App())
|
||||||
|
|
||||||
|
if utils.IsCloudLicense(p.API.GetLicense()) {
|
||||||
|
limits, err := p.API.GetCloudLimits()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("ERROR FETCHING CLOUD LIMITS WHEN STARTING THE PLUGIN", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.App().SetCloudLimits(limits); err != nil {
|
||||||
|
fmt.Println("ERROR SETTING CLOUD LIMITS WHEN STARTING THE PLUGIN", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.server = server
|
p.server = server
|
||||||
return server.Start()
|
return server.Start()
|
||||||
}
|
}
|
||||||
@ -510,3 +526,9 @@ func isBoardsLink(link string) bool {
|
|||||||
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||||
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
|
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) OnCloudLimitsUpdated(limits *mmModel.ProductLimits) {
|
||||||
|
if err := p.server.App().SetCloudLimits(limits); err != nil {
|
||||||
|
fmt.Println("Error setting the cloud limits for Boards", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -39,7 +39,10 @@ import wsClient, {
|
|||||||
ACTION_UPDATE_BLOCK,
|
ACTION_UPDATE_BLOCK,
|
||||||
ACTION_UPDATE_CLIENT_CONFIG,
|
ACTION_UPDATE_CLIENT_CONFIG,
|
||||||
ACTION_UPDATE_SUBSCRIPTION,
|
ACTION_UPDATE_SUBSCRIPTION,
|
||||||
ACTION_UPDATE_CATEGORY, ACTION_UPDATE_BOARD_CATEGORY, ACTION_UPDATE_BOARD,
|
ACTION_UPDATE_CARD_LIMIT_TIMESTAMP,
|
||||||
|
ACTION_UPDATE_CATEGORY,
|
||||||
|
ACTION_UPDATE_BOARD_CATEGORY,
|
||||||
|
ACTION_UPDATE_BOARD,
|
||||||
} from './../../../webapp/src/wsclient'
|
} from './../../../webapp/src/wsclient'
|
||||||
|
|
||||||
import manifest from './manifest'
|
import manifest from './manifest'
|
||||||
@ -275,6 +278,7 @@ export default class Plugin {
|
|||||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BOARD_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BOARD_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data))
|
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data))
|
||||||
|
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CARD_LIMIT_TIMESTAMP}`, (e: any) => wsClient.updateCardLimitTimestampHandler(e.data))
|
||||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data))
|
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data))
|
||||||
this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data))
|
this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data))
|
||||||
this.registry?.registerWebSocketEventHandler('preferences_changed', (e: any) => {
|
this.registry?.registerWebSocketEventHandler('preferences_changed', (e: any) => {
|
||||||
|
@ -153,6 +153,9 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||||||
apiv2.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET")
|
apiv2.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET")
|
||||||
apiv2.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
|
apiv2.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
|
||||||
|
|
||||||
|
// limits
|
||||||
|
apiv2.HandleFunc("/limits", a.sessionRequired(a.handleCloudLimits)).Methods("GET")
|
||||||
|
|
||||||
// System APIs
|
// System APIs
|
||||||
r.HandleFunc("/hello", a.handleHello).Methods("GET")
|
r.HandleFunc("/hello", a.handleHello).Methods("GET")
|
||||||
}
|
}
|
||||||
@ -375,6 +378,13 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
mlog.Int("block_count", len(blocks)),
|
mlog.Int("block_count", len(blocks)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var bErr error
|
||||||
|
blocks, bErr = a.app.ApplyCloudLimits(blocks)
|
||||||
|
if bErr != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", bErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
json, err := json.Marshal(blocks)
|
json, err := json.Marshal(blocks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -875,7 +885,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
newBlocks, err := a.app.InsertBlocks(blocks, session.UserID, true)
|
newBlocks, err := a.app.InsertBlocks(blocks, session.UserID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
if errors.Is(err, app.ErrViewsLimitReached) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
|
||||||
|
} else {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1408,6 +1423,10 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.AddMeta("blockID", blockID)
|
auditRec.AddMeta("blockID", blockID)
|
||||||
|
|
||||||
err = a.app.PatchBlock(blockID, patch, userID)
|
err = a.app.PatchBlock(blockID, patch, userID)
|
||||||
|
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
@ -1489,6 +1508,10 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = a.app.PatchBlocks(teamID, patches, userID)
|
err = a.app.PatchBlocks(teamID, patches, userID)
|
||||||
|
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
@ -1830,7 +1853,7 @@ func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http
|
|||||||
// File upload
|
// File upload
|
||||||
|
|
||||||
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:operation GET "api/v2/files/teams/{teamID}/{boardID}/{filename} getFile
|
// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename} getFile
|
||||||
//
|
//
|
||||||
// Returns the contents of an uploaded file
|
// Returns the contents of an uploaded file
|
||||||
//
|
//
|
||||||
@ -4002,6 +4025,10 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
|
|||||||
auditRec.AddMeta("blocksCount", len(pbab.BlockIDs))
|
auditRec.AddMeta("blocksCount", len(pbab.BlockIDs))
|
||||||
|
|
||||||
bab, err := a.app.PatchBoardsAndBlocks(pbab, userID)
|
bab, err := a.app.PatchBoardsAndBlocks(pbab, userID)
|
||||||
|
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
@ -4141,6 +4168,41 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
|||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *API) handleCloudLimits(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:operation GET /limits cloudLimits
|
||||||
|
//
|
||||||
|
// Fetches the cloud limits of the server.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// security:
|
||||||
|
// - BearerAuth: []
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: success
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/BoardsCloudLimits"
|
||||||
|
// default:
|
||||||
|
// description: internal error
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
|
||||||
|
boardsCloudLimits, err := a.app.GetBoardsCloudLimits()
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(boardsCloudLimits)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytesResponse(w, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *API) handleHello(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleHello(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:operation GET /hello hello
|
// swagger:operation GET /hello hello
|
||||||
//
|
//
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/focalboard/server/auth"
|
"github.com/mattermost/focalboard/server/auth"
|
||||||
@ -13,9 +14,10 @@ import (
|
|||||||
"github.com/mattermost/focalboard/server/utils"
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
"github.com/mattermost/focalboard/server/ws"
|
"github.com/mattermost/focalboard/server/ws"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -24,6 +26,10 @@ const (
|
|||||||
blockChangeNotifierShutdownTimeout = time.Second * 10
|
blockChangeNotifierShutdownTimeout = time.Second * 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type pluginAPI interface {
|
||||||
|
GetUsers(options *mmModel.UserGetOptions) ([]*mmModel.User, *mmModel.AppError)
|
||||||
|
}
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
Auth *auth.Auth
|
Auth *auth.Auth
|
||||||
Store store.Store
|
Store store.Store
|
||||||
@ -34,6 +40,7 @@ type Services struct {
|
|||||||
Logger *mlog.Logger
|
Logger *mlog.Logger
|
||||||
Permissions permissions.PermissionsService
|
Permissions permissions.PermissionsService
|
||||||
SkipTemplateInit bool
|
SkipTemplateInit bool
|
||||||
|
PluginAPI pluginAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
@ -47,6 +54,10 @@ type App struct {
|
|||||||
notifications *notify.Service
|
notifications *notify.Service
|
||||||
logger *mlog.Logger
|
logger *mlog.Logger
|
||||||
blockChangeNotifier *utils.CallbackQueue
|
blockChangeNotifier *utils.CallbackQueue
|
||||||
|
pluginAPI pluginAPI
|
||||||
|
|
||||||
|
cardLimitMux sync.RWMutex
|
||||||
|
cardLimit int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SetConfig(config *config.Configuration) {
|
func (a *App) SetConfig(config *config.Configuration) {
|
||||||
@ -69,7 +80,20 @@ func New(config *config.Configuration, wsAdapter ws.Adapter, services Services)
|
|||||||
notifications: services.Notifications,
|
notifications: services.Notifications,
|
||||||
logger: services.Logger,
|
logger: services.Logger,
|
||||||
blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger),
|
blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger),
|
||||||
|
pluginAPI: services.PluginAPI,
|
||||||
}
|
}
|
||||||
app.initialize(services.SkipTemplateInit)
|
app.initialize(services.SkipTemplateInit)
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) CardLimit() int {
|
||||||
|
a.cardLimitMux.RLock()
|
||||||
|
defer a.cardLimitMux.RUnlock()
|
||||||
|
return a.cardLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SetCardLimit(cardLimit int) {
|
||||||
|
a.cardLimitMux.Lock()
|
||||||
|
defer a.cardLimitMux.Unlock()
|
||||||
|
a.cardLimit = cardLimit
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
@ -114,7 +113,6 @@ func TestRegisterUser(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range testcases {
|
for _, test := range testcases {
|
||||||
t.Run(test.title, func(t *testing.T) {
|
t.Run(test.title, func(t *testing.T) {
|
||||||
fmt.Println(test.email)
|
|
||||||
err := th.App.RegisterUser(test.userName, test.email, test.password)
|
err := th.App.RegisterUser(test.userName, test.email, test.password)
|
||||||
if test.isError {
|
if test.isError {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
|
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
|
||||||
|
var ErrViewsLimitReached = errors.New("views limit reached for board")
|
||||||
|
var ErrPatchUpdatesLimitedCards = errors.New("patch updates cards that are limited")
|
||||||
|
|
||||||
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) {
|
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) {
|
||||||
if boardID == "" {
|
if boardID == "" {
|
||||||
@ -50,6 +52,16 @@ func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTe
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed duplicating a block",
|
||||||
|
mlog.Err(uErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return blocks, err
|
return blocks, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +72,17 @@ func (a *App) GetBlocksWithBoardID(boardID string) ([]model.Block, error) {
|
|||||||
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
||||||
oldBlock, err := a.store.GetBlock(blockID)
|
oldBlock, err := a.store.GetBlock(blockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.IsCloudLimited() {
|
||||||
|
containsLimitedBlocks, lErr := a.ContainsLimitedBlocks([]model.Block{*oldBlock})
|
||||||
|
if lErr != nil {
|
||||||
|
return lErr
|
||||||
|
}
|
||||||
|
if containsLimitedBlocks {
|
||||||
|
return ErrPatchUpdatesLimitedCards
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
board, err := a.store.GetBoard(oldBlock.BoardID)
|
board, err := a.store.GetBoard(oldBlock.BoardID)
|
||||||
@ -76,7 +98,7 @@ func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedB
|
|||||||
a.metrics.IncrementBlocksPatched(1)
|
a.metrics.IncrementBlocksPatched(1)
|
||||||
block, err := a.store.GetBlock(blockID)
|
block, err := a.store.GetBlock(blockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
a.blockChangeNotifier.Enqueue(func() error {
|
a.blockChangeNotifier.Enqueue(func() error {
|
||||||
// broadcast on websocket
|
// broadcast on websocket
|
||||||
@ -93,17 +115,22 @@ func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedB
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
|
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
|
||||||
oldBlocks := make([]model.Block, 0, len(blockPatches.BlockIDs))
|
oldBlocks, err := a.store.GetBlocksByIDs(blockPatches.BlockIDs)
|
||||||
for _, blockID := range blockPatches.BlockIDs {
|
if err != nil {
|
||||||
oldBlock, err := a.store.GetBlock(blockID)
|
return err
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
oldBlocks = append(oldBlocks, *oldBlock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.store.PatchBlocks(blockPatches, modifiedByID)
|
if a.IsCloudLimited() {
|
||||||
if err != nil {
|
containsLimitedBlocks, err := a.ContainsLimitedBlocks(oldBlocks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if containsLimitedBlocks {
|
||||||
|
return ErrPatchUpdatesLimitedCards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.store.PatchBlocks(blockPatches, modifiedByID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +139,7 @@ func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, mo
|
|||||||
for i, blockID := range blockPatches.BlockIDs {
|
for i, blockID := range blockPatches.BlockIDs {
|
||||||
newBlock, err := a.store.GetBlock(blockID)
|
newBlock, err := a.store.GetBlock(blockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
a.wsAdapter.BroadcastBlockChange(teamID, *newBlock)
|
a.wsAdapter.BroadcastBlockChange(teamID, *newBlock)
|
||||||
a.webhook.NotifyUpdate(*newBlock)
|
a.webhook.NotifyUpdate(*newBlock)
|
||||||
@ -136,9 +163,20 @@ func (a *App) InsertBlock(block model.Block, modifiedByID string) error {
|
|||||||
a.metrics.IncrementBlocksInserted(1)
|
a.metrics.IncrementBlocksInserted(1)
|
||||||
a.webhook.NotifyUpdate(block)
|
a.webhook.NotifyUpdate(block)
|
||||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after inserting a block",
|
||||||
|
mlog.Err(uErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,9 +218,19 @@ func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotif
|
|||||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after inserting blocks",
|
||||||
|
mlog.Err(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return blocks, nil
|
return blocks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,8 +337,19 @@ func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
|
|||||||
a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID)
|
a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID)
|
||||||
a.metrics.IncrementBlocksDeleted(1)
|
a.metrics.IncrementBlocksDeleted(1)
|
||||||
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
|
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after deleting a block",
|
||||||
|
mlog.Err(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,9 +400,19 @@ func (a *App) UndeleteBlock(blockID string, modifiedBy string) (*model.Block, er
|
|||||||
a.metrics.IncrementBlocksInserted(1)
|
a.metrics.IncrementBlocksInserted(1)
|
||||||
a.webhook.NotifyUpdate(*block)
|
a.webhook.NotifyUpdate(*block)
|
||||||
a.notifyBlockChanged(notify.Add, block, nil, modifiedBy)
|
a.notifyBlockChanged(notify.Add, block, nil, modifiedBy)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after undeleting a block",
|
||||||
|
mlog.Err(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return block, nil
|
return block, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/mattermost/focalboard/server/model"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type blockError struct {
|
type blockError struct {
|
||||||
@ -48,17 +51,63 @@ func TestPatchBlocks(t *testing.T) {
|
|||||||
defer tearDown()
|
defer tearDown()
|
||||||
|
|
||||||
t.Run("patchBlocks success scenerio", func(t *testing.T) {
|
t.Run("patchBlocks success scenerio", func(t *testing.T) {
|
||||||
blockPatches := model.BlockPatchBatch{}
|
blockPatches := model.BlockPatchBatch{
|
||||||
|
BlockIDs: []string{"block1"},
|
||||||
|
BlockPatches: []model.BlockPatch{
|
||||||
|
{Title: mmModel.NewString("new title")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
block1 := model.Block{ID: "block1"}
|
||||||
|
th.Store.EXPECT().GetBlocksByIDs([]string{"block1"}).Return([]model.Block{block1}, nil)
|
||||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil)
|
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil)
|
||||||
|
th.Store.EXPECT().GetBlock("block1").Return(&block1, nil)
|
||||||
|
// this call comes from the WS server notification
|
||||||
|
th.Store.EXPECT().GetMembersForBoard(gomock.Any()).Times(1)
|
||||||
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("patchBlocks error scenerio", func(t *testing.T) {
|
t.Run("patchBlocks error scenerio", func(t *testing.T) {
|
||||||
blockPatches := model.BlockPatchBatch{}
|
blockPatches := model.BlockPatchBatch{BlockIDs: []string{}}
|
||||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
th.Store.EXPECT().GetBlocksByIDs([]string{}).Return(nil, sql.ErrNoRows)
|
||||||
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||||
require.Error(t, err, "error")
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cloud limit error scenario", func(t *testing.T) {
|
||||||
|
th.App.SetCardLimit(5)
|
||||||
|
|
||||||
|
fakeLicense := &mmModel.License{
|
||||||
|
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||||
|
}
|
||||||
|
|
||||||
|
blockPatches := model.BlockPatchBatch{
|
||||||
|
BlockIDs: []string{"block1"},
|
||||||
|
BlockPatches: []model.BlockPatch{
|
||||||
|
{Title: mmModel.NewString("new title")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
block1 := model.Block{
|
||||||
|
ID: "block1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board-id",
|
||||||
|
BoardID: "board-id",
|
||||||
|
UpdateAt: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board-id",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
}
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetBlocksByIDs([]string{"block1"}).Return([]model.Block{block1}, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board-id").Return(board1, nil)
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil)
|
||||||
|
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||||
|
require.ErrorIs(t, err, ErrPatchUpdatesLimitedCards)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -199,6 +202,18 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if len(bab.Blocks) != 0 {
|
||||||
|
go func() {
|
||||||
|
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after duplicating a board",
|
||||||
|
mlog.Err(uErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return bab, members, err
|
return bab, members, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,6 +288,15 @@ func (a *App) DeleteBoard(boardID, userID string) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after deleting a board",
|
||||||
|
mlog.Err(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,5 +475,14 @@ func (a *App) UndeleteBoard(boardID string, modifiedBy string) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after undeleting a board",
|
||||||
|
mlog.Err(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -44,17 +44,39 @@ func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(newBab.Blocks) != 0 {
|
||||||
|
go func() {
|
||||||
|
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after creating boards and blocks",
|
||||||
|
mlog.Err(uErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return newBab, nil
|
return newBab, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
|
func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
|
||||||
oldBlocksMap := map[string]*model.Block{}
|
oldBlocks, err := a.store.GetBlocksByIDs(pbab.BlockIDs)
|
||||||
for _, blockID := range pbab.BlockIDs {
|
if err != nil {
|
||||||
block, err := a.store.GetBlock(blockID)
|
return nil, err
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
|
if a.IsCloudLimited() {
|
||||||
|
containsLimitedBlocks, cErr := a.ContainsLimitedBlocks(oldBlocks)
|
||||||
|
if cErr != nil {
|
||||||
|
return nil, cErr
|
||||||
}
|
}
|
||||||
oldBlocksMap[blockID] = block
|
if containsLimitedBlocks {
|
||||||
|
return nil, ErrPatchUpdatesLimitedCards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldBlocksMap := map[string]model.Block{}
|
||||||
|
for _, block := range oldBlocks {
|
||||||
|
oldBlocksMap[block.ID] = block
|
||||||
}
|
}
|
||||||
|
|
||||||
bab, err := a.store.PatchBoardsAndBlocks(pbab, userID)
|
bab, err := a.store.PatchBoardsAndBlocks(pbab, userID)
|
||||||
@ -76,7 +98,7 @@ func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID stri
|
|||||||
a.metrics.IncrementBlocksPatched(1)
|
a.metrics.IncrementBlocksPatched(1)
|
||||||
a.wsAdapter.BroadcastBlockChange(teamID, b)
|
a.wsAdapter.BroadcastBlockChange(teamID, b)
|
||||||
a.webhook.NotifyUpdate(b)
|
a.webhook.NotifyUpdate(b)
|
||||||
a.notifyBlockChanged(notify.Update, &b, oldBlock, userID)
|
a.notifyBlockChanged(notify.Update, &b, &oldBlock, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, board := range bab.Boards {
|
for _, board := range bab.Boards {
|
||||||
@ -122,5 +144,16 @@ func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID st
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if len(dbab.Blocks) != 0 {
|
||||||
|
go func() {
|
||||||
|
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after deleting boards and blocks",
|
||||||
|
mlog.Err(uErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
256
server/app/cloud.go
Normal file
256
server/app/cloud.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNilPluginAPI = errors.New("server not running in plugin mode")
|
||||||
|
|
||||||
|
// GetBoardsCloudLimits returns the limits of the server, and an empty
|
||||||
|
// limits struct if there are no limits set.
|
||||||
|
func (a *App) GetBoardsCloudLimits() (*model.BoardsCloudLimits, error) {
|
||||||
|
if !a.IsCloud() {
|
||||||
|
return &model.BoardsCloudLimits{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
productLimits, err := a.store.GetCloudLimits()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
usedCards, err := a.store.GetUsedCardsCount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
boardsCloudLimits := &model.BoardsCloudLimits{
|
||||||
|
UsedCards: usedCards,
|
||||||
|
CardLimitTimestamp: cardLimitTimestamp,
|
||||||
|
}
|
||||||
|
if productLimits != nil && productLimits.Boards != nil {
|
||||||
|
if productLimits.Boards.Cards != nil {
|
||||||
|
boardsCloudLimits.Cards = *productLimits.Boards.Cards
|
||||||
|
}
|
||||||
|
if productLimits.Boards.Views != nil {
|
||||||
|
boardsCloudLimits.Views = *productLimits.Boards.Views
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return boardsCloudLimits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCloud returns true if the server is running as a plugin in a
|
||||||
|
// cloud licensed server.
|
||||||
|
func (a *App) IsCloud() bool {
|
||||||
|
return utils.IsCloudLicense(a.store.GetLicense())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCloudLimited returns true if the server is running in cloud mode
|
||||||
|
// and the card limit has been set.
|
||||||
|
func (a *App) IsCloudLimited() bool {
|
||||||
|
return a.CardLimit() != 0 && a.IsCloud()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCloudLimits sets the limits of the server.
|
||||||
|
func (a *App) SetCloudLimits(limits *mmModel.ProductLimits) error {
|
||||||
|
oldCardLimit := a.CardLimit()
|
||||||
|
|
||||||
|
// if the limit object doesn't come complete, we assume limits are
|
||||||
|
// being disabled
|
||||||
|
cardLimit := 0
|
||||||
|
if limits != nil && limits.Boards != nil && limits.Boards.Cards != nil {
|
||||||
|
cardLimit = *limits.Boards.Cards
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCardLimit != cardLimit {
|
||||||
|
a.SetCardLimit(cardLimit)
|
||||||
|
return a.doUpdateCardLimitTimestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// doUpdateCardLimitTimestamp performs the update without running any
|
||||||
|
// checks.
|
||||||
|
func (a *App) doUpdateCardLimitTimestamp() error {
|
||||||
|
cardLimitTimestamp, err := a.store.UpdateCardLimitTimestamp(a.CardLimit())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.wsAdapter.BroadcastCardLimitTimestampChange(cardLimitTimestamp)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCardLimitTimestamp checks if the server is a cloud instance
|
||||||
|
// with limits applied, and if that's true, recalculates the card
|
||||||
|
// limit timestamp and propagates the new one to the connected
|
||||||
|
// clients.
|
||||||
|
func (a *App) UpdateCardLimitTimestamp() error {
|
||||||
|
if !a.IsCloudLimited() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.doUpdateCardLimitTimestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTemplateMapForBlocks gets all board ids for the blocks, and
|
||||||
|
// builds a map with the board IDs as the key and their isTemplate
|
||||||
|
// field as the value.
|
||||||
|
func (a *App) getTemplateMapForBlocks(blocks []model.Block) (map[string]bool, error) {
|
||||||
|
boardMap := map[string]*model.Board{}
|
||||||
|
for _, block := range blocks {
|
||||||
|
if _, ok := boardMap[block.BoardID]; !ok {
|
||||||
|
board, err := a.store.GetBoard(block.BoardID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
boardMap[block.BoardID] = board
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templateMap := map[string]bool{}
|
||||||
|
for boardID, board := range boardMap {
|
||||||
|
templateMap[boardID] = board.IsTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
return templateMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyCloudLimits takes a set of blocks and, if the server is cloud
|
||||||
|
// limited, limits those that are outside of the card limit and don't
|
||||||
|
// belong to a template.
|
||||||
|
func (a *App) ApplyCloudLimits(blocks []model.Block) ([]model.Block, error) {
|
||||||
|
// if there is no limit currently being applied, return
|
||||||
|
if !a.IsCloudLimited() {
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
templateMap, err := a.getTemplateMapForBlocks(blocks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
limitedBlocks := make([]model.Block, len(blocks))
|
||||||
|
for i, block := range blocks {
|
||||||
|
// if the block belongs to a template, it will never be
|
||||||
|
// limited
|
||||||
|
if isTemplate, ok := templateMap[block.BoardID]; ok && isTemplate {
|
||||||
|
limitedBlocks[i] = block
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if block.ShouldBeLimited(cardLimitTimestamp) {
|
||||||
|
limitedBlocks[i] = block.GetLimited()
|
||||||
|
} else {
|
||||||
|
limitedBlocks[i] = block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return limitedBlocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsLimitedBlocks checks if a list of blocks contain any block
|
||||||
|
// that references a limited card.
|
||||||
|
func (a *App) ContainsLimitedBlocks(blocks []model.Block) (bool, error) {
|
||||||
|
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cardLimitTimestamp == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cards := []model.Block{}
|
||||||
|
cardIDMap := map[string]bool{}
|
||||||
|
for _, block := range blocks {
|
||||||
|
switch block.Type {
|
||||||
|
case model.TypeCard:
|
||||||
|
cards = append(cards, block)
|
||||||
|
default:
|
||||||
|
cardIDMap[block.ParentID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cardIDs := []string{}
|
||||||
|
// if the card is already present on the set, we don't need to
|
||||||
|
// fetch it from the database
|
||||||
|
for cardID := range cardIDMap {
|
||||||
|
alreadyPresent := false
|
||||||
|
for _, card := range cards {
|
||||||
|
if card.ID == cardID {
|
||||||
|
alreadyPresent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !alreadyPresent {
|
||||||
|
cardIDs = append(cardIDs, cardID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cardIDs) > 0 {
|
||||||
|
fetchedCards, fErr := a.store.GetBlocksByIDs(cardIDs)
|
||||||
|
if fErr != nil {
|
||||||
|
return false, fErr
|
||||||
|
}
|
||||||
|
cards = append(cards, fetchedCards...)
|
||||||
|
}
|
||||||
|
|
||||||
|
templateMap, err := a.getTemplateMapForBlocks(cards)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, card := range cards {
|
||||||
|
isTemplate, ok := templateMap[card.BoardID]
|
||||||
|
if !ok {
|
||||||
|
return false, newErrBoardNotFoundInTemplateMap(card.BoardID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the block belongs to a template, it will never be
|
||||||
|
// limited
|
||||||
|
if isTemplate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if card.ShouldBeLimited(cardLimitTimestamp) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type errBoardNotFoundInTemplateMap struct {
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newErrBoardNotFoundInTemplateMap(id string) *errBoardNotFoundInTemplateMap {
|
||||||
|
return &errBoardNotFoundInTemplateMap{id}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *errBoardNotFoundInTemplateMap) Error() string {
|
||||||
|
return fmt.Sprintf("board %q not found in template map", eb.id)
|
||||||
|
}
|
642
server/app/cloud_test.go
Normal file
642
server/app/cloud_test.go
Normal file
@ -0,0 +1,642 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsCloud(t *testing.T) {
|
||||||
|
t.Run("if it's not running on plugin mode", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetLicense().Return(nil)
|
||||||
|
require.False(t, th.App.IsCloud())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if it's running on plugin mode but the license is incomplete", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
fakeLicense := &mmModel.License{}
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
require.False(t, th.App.IsCloud())
|
||||||
|
|
||||||
|
fakeLicense = &mmModel.License{Features: &mmModel.Features{}}
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
require.False(t, th.App.IsCloud())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if it's running on plugin mode, with a non-cloud license", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
fakeLicense := &mmModel.License{
|
||||||
|
Features: &mmModel.Features{Cloud: mmModel.NewBool(false)},
|
||||||
|
}
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
require.False(t, th.App.IsCloud())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if it's running on plugin mode with a cloud license", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
fakeLicense := &mmModel.License{
|
||||||
|
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||||
|
}
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
require.True(t, th.App.IsCloud())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsCloudLimited(t *testing.T) {
|
||||||
|
t.Run("if no limit has been set, it should be false", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
require.False(t, th.App.IsCloudLimited())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if the limit is set, it should be true", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
fakeLicense := &mmModel.License{
|
||||||
|
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||||
|
}
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
|
||||||
|
th.App.SetCardLimit(5)
|
||||||
|
require.True(t, th.App.IsCloudLimited())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCloudLimits(t *testing.T) {
|
||||||
|
t.Run("if the limits are empty, it should do nothing", func(t *testing.T) {
|
||||||
|
t.Run("limits empty", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
|
||||||
|
require.NoError(t, th.App.SetCloudLimits(nil))
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("limits not empty but board limits empty", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
|
||||||
|
limits := &mmModel.ProductLimits{}
|
||||||
|
|
||||||
|
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("limits not empty but board limits values empty", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
|
||||||
|
limits := &mmModel.ProductLimits{
|
||||||
|
Boards: &mmModel.BoardsLimits{},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if the limits are not empty, it should update them and calculate the new timestamp", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
|
||||||
|
newCardLimitTimestamp := int64(27)
|
||||||
|
th.Store.EXPECT().UpdateCardLimitTimestamp(5).Return(newCardLimitTimestamp, nil)
|
||||||
|
|
||||||
|
limits := &mmModel.ProductLimits{
|
||||||
|
Boards: &mmModel.BoardsLimits{Cards: mmModel.NewInt(5)},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||||
|
require.Equal(t, 5, th.App.CardLimit())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if the limits are already set and we unset them, the timestamp will be unset too", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
th.App.SetCardLimit(20)
|
||||||
|
|
||||||
|
th.Store.EXPECT().UpdateCardLimitTimestamp(0)
|
||||||
|
|
||||||
|
require.NoError(t, th.App.SetCloudLimits(nil))
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if the limits are already set and we try to set the same ones again", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
th.App.SetCardLimit(20)
|
||||||
|
|
||||||
|
// the call to update card limit timestamp should not happen
|
||||||
|
// as the limits didn't change
|
||||||
|
th.Store.EXPECT().UpdateCardLimitTimestamp(gomock.Any()).Times(0)
|
||||||
|
|
||||||
|
limits := &mmModel.ProductLimits{
|
||||||
|
Boards: &mmModel.BoardsLimits{Cards: mmModel.NewInt(20)},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||||
|
require.Equal(t, 20, th.App.CardLimit())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateCardLimitTimestamp(t *testing.T) {
|
||||||
|
fakeLicense := &mmModel.License{
|
||||||
|
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("if the server is a cloud instance but not limited, it should do nothing", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
|
||||||
|
// the license check will not be done as the limit not being
|
||||||
|
// set is enough for the method to return
|
||||||
|
th.Store.EXPECT().GetLicense().Times(0)
|
||||||
|
// no call to UpdateCardLimitTimestamp should happen as the
|
||||||
|
// method should shortcircuit if not cloud limited
|
||||||
|
th.Store.EXPECT().UpdateCardLimitTimestamp(gomock.Any()).Times(0)
|
||||||
|
|
||||||
|
require.NoError(t, th.App.UpdateCardLimitTimestamp())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if the server is a cloud instance and the timestamp is set, it should run the update", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
th.App.SetCardLimit(5)
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
// no call to UpdateCardLimitTimestamp should happen as the
|
||||||
|
// method should shortcircuit if not cloud limited
|
||||||
|
th.Store.EXPECT().UpdateCardLimitTimestamp(5)
|
||||||
|
|
||||||
|
require.NoError(t, th.App.UpdateCardLimitTimestamp())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTemplateMapForBlocks(t *testing.T) {
|
||||||
|
t.Run("should fetch the necessary boards from the database", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board1",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
IsTemplate: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
board2 := &model.Board{
|
||||||
|
ID: "board2",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
IsTemplate: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "card2",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board2",
|
||||||
|
BoardID: "board2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "text2",
|
||||||
|
Type: model.TypeText,
|
||||||
|
ParentID: "card2",
|
||||||
|
BoardID: "board2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
th.Store.EXPECT().
|
||||||
|
GetBoard("board1").
|
||||||
|
Return(board1, nil).
|
||||||
|
Times(1)
|
||||||
|
th.Store.EXPECT().
|
||||||
|
GetBoard("board2").
|
||||||
|
Return(board2, nil).
|
||||||
|
Times(1)
|
||||||
|
|
||||||
|
templateMap, err := th.App.getTemplateMapForBlocks(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, templateMap, 2)
|
||||||
|
require.Contains(t, templateMap, "board1")
|
||||||
|
require.True(t, templateMap["board1"])
|
||||||
|
require.Contains(t, templateMap, "board2")
|
||||||
|
require.False(t, templateMap["board2"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail if the board is not in the database", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "card2",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board2",
|
||||||
|
BoardID: "board2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
th.Store.EXPECT().
|
||||||
|
GetBoard("board1").
|
||||||
|
Return(nil, sql.ErrNoRows).
|
||||||
|
Times(1)
|
||||||
|
|
||||||
|
templateMap, err := th.App.getTemplateMapForBlocks(blocks)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
require.Empty(t, templateMap)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCloudLimits(t *testing.T) {
|
||||||
|
fakeLicense := &mmModel.License{
|
||||||
|
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||||
|
}
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board1",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
IsTemplate: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &model.Board{
|
||||||
|
ID: "template",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
IsTemplate: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "text1",
|
||||||
|
Type: model.TypeText,
|
||||||
|
ParentID: "card1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "card2",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "card-from-template",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "template",
|
||||||
|
BoardID: "template",
|
||||||
|
UpdateAt: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("if the server is not limited, it should return the blocks untouched", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Zero(t, th.App.CardLimit())
|
||||||
|
|
||||||
|
newBlocks, err := th.App.ApplyCloudLimits(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ElementsMatch(t, blocks, newBlocks)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("if the server is limited, it should limit the blocks that are beyond the card limit timestamp", func(t *testing.T) {
|
||||||
|
findBlock := func(blocks []model.Block, id string) model.Block {
|
||||||
|
for _, block := range blocks {
|
||||||
|
if block.ID == id {
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.FailNow(t, "block %s not found", id)
|
||||||
|
return model.Block{} // this should never be reached
|
||||||
|
}
|
||||||
|
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
th.App.SetCardLimit(5)
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board1").Return(board1, nil).Times(1)
|
||||||
|
th.Store.EXPECT().GetBoard("template").Return(template, nil).Times(1)
|
||||||
|
|
||||||
|
newBlocks, err := th.App.ApplyCloudLimits(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// should be limited as it's beyond the threshold
|
||||||
|
require.True(t, findBlock(newBlocks, "card1").Limited)
|
||||||
|
// only cards are limited
|
||||||
|
require.False(t, findBlock(newBlocks, "text1").Limited)
|
||||||
|
// should not be limited as it's not beyond the threshold
|
||||||
|
require.False(t, findBlock(newBlocks, "card2").Limited)
|
||||||
|
// cards belonging to templates are never limited
|
||||||
|
require.False(t, findBlock(newBlocks, "card-from-template").Limited)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainsLimitedBlocks(t *testing.T) {
|
||||||
|
// for all the following tests, the timestamp will be set to 150,
|
||||||
|
// which means that blocks with an UpdateAt set to 100 will be
|
||||||
|
// outside the active window and possibly limited, and blocks with
|
||||||
|
// UpdateAt set to 200 will not
|
||||||
|
|
||||||
|
t.Run("should return false if the card limit timestamp is zero", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(0), nil)
|
||||||
|
|
||||||
|
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, containsLimitedBlocks)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return true if the block set contains a card that is limited", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board1",
|
||||||
|
Type: model.BoardTypePrivate,
|
||||||
|
}
|
||||||
|
|
||||||
|
th.App.SetCardLimit(500)
|
||||||
|
cardLimitTimestamp := int64(150)
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||||
|
|
||||||
|
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, containsLimitedBlocks)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return false if that same block belongs to a template", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board1",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
IsTemplate: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
th.App.SetCardLimit(500)
|
||||||
|
cardLimitTimestamp := int64(150)
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||||
|
|
||||||
|
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, containsLimitedBlocks)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return true if the block contains a content block that belongs to a card that should be limited", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "text1",
|
||||||
|
Type: model.TypeText,
|
||||||
|
ParentID: "card1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
card1 := model.Block{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board1",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
}
|
||||||
|
|
||||||
|
th.App.SetCardLimit(500)
|
||||||
|
cardLimitTimestamp := int64(150)
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||||
|
th.Store.EXPECT().GetBlocksByIDs([]string{"card1"}).Return([]model.Block{card1}, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||||
|
|
||||||
|
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, containsLimitedBlocks)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return false if that same block belongs to a card that is inside the active window", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
{
|
||||||
|
ID: "text1",
|
||||||
|
Type: model.TypeText,
|
||||||
|
ParentID: "card1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
card1 := model.Block{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board1",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
}
|
||||||
|
|
||||||
|
th.App.SetCardLimit(500)
|
||||||
|
cardLimitTimestamp := int64(150)
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||||
|
th.Store.EXPECT().GetBlocksByIDs([]string{"card1"}).Return([]model.Block{card1}, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||||
|
|
||||||
|
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, containsLimitedBlocks)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should reach to the database to fetch the necessary information only in an efficient way", func(t *testing.T) {
|
||||||
|
th, tearDown := SetupTestHelper(t)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
blocks := []model.Block{
|
||||||
|
// a content block that references a card that needs
|
||||||
|
// fetching
|
||||||
|
{
|
||||||
|
ID: "text1",
|
||||||
|
Type: model.TypeText,
|
||||||
|
ParentID: "card1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
// a board that needs fetching referenced by a card and a content block
|
||||||
|
{
|
||||||
|
ID: "card2",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board2",
|
||||||
|
BoardID: "board2",
|
||||||
|
// per timestamp should be limited but the board is a
|
||||||
|
// template
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "text2",
|
||||||
|
Type: model.TypeText,
|
||||||
|
ParentID: "card2",
|
||||||
|
BoardID: "board2",
|
||||||
|
UpdateAt: 200,
|
||||||
|
},
|
||||||
|
// a content block that references a card and a board,
|
||||||
|
// both absent
|
||||||
|
{
|
||||||
|
ID: "image3",
|
||||||
|
Type: model.TypeImage,
|
||||||
|
ParentID: "card3",
|
||||||
|
BoardID: "board3",
|
||||||
|
UpdateAt: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
card1 := model.Block{
|
||||||
|
ID: "card1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
UpdateAt: 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
card3 := model.Block{
|
||||||
|
ID: "card3",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
ParentID: "board3",
|
||||||
|
BoardID: "board3",
|
||||||
|
UpdateAt: 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
board1 := &model.Board{
|
||||||
|
ID: "board1",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
}
|
||||||
|
|
||||||
|
board2 := &model.Board{
|
||||||
|
ID: "board2",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
IsTemplate: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
board3 := &model.Board{
|
||||||
|
ID: "board3",
|
||||||
|
Type: model.BoardTypePrivate,
|
||||||
|
}
|
||||||
|
|
||||||
|
th.App.SetCardLimit(500)
|
||||||
|
cardLimitTimestamp := int64(150)
|
||||||
|
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||||
|
th.Store.EXPECT().GetBlocksByIDs(gomock.InAnyOrder([]string{"card1", "card3"})).Return([]model.Block{card1, card3}, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board2").Return(board2, nil)
|
||||||
|
th.Store.EXPECT().GetBoard("board3").Return(board3, nil)
|
||||||
|
|
||||||
|
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, containsLimitedBlocks)
|
||||||
|
})
|
||||||
|
}
|
@ -40,6 +40,16 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
|||||||
if err == nil && string(peek) == legacyFileBegin {
|
if err == nil && string(peek) == legacyFileBegin {
|
||||||
a.logger.Debug("importing legacy archive")
|
a.logger.Debug("importing legacy archive")
|
||||||
_, errImport := a.ImportBoardJSONL(br, opt)
|
_, errImport := a.ImportBoardJSONL(br, opt)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after importing a legacy file",
|
||||||
|
mlog.Err(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return errImport
|
return errImport
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +108,15 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
|||||||
mlog.String("dir", dir),
|
mlog.String("dir", dir),
|
||||||
mlog.String("filename", filename),
|
mlog.String("filename", filename),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||||
|
a.logger.Error(
|
||||||
|
"UpdateCardLimitTimestamp failed after importing an archive",
|
||||||
|
mlog.Err(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,3 +730,19 @@ func (c *Client) ImportArchive(teamID string, data io.Reader) *Response {
|
|||||||
|
|
||||||
return BuildResponse(r)
|
return BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetLimits() (*model.BoardsCloudLimits, *Response) {
|
||||||
|
r, err := c.DoAPIGet("/limits", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
var limits *model.BoardsCloudLimits
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&limits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return limits, BuildResponse(r)
|
||||||
|
}
|
||||||
|
@ -30,6 +30,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||||
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
@ -210,6 +210,7 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
|
|||||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
github.com/blevesearch/bleve/v2 v2.3.2/go.mod h1:96+xE5pZUOsr3Y4vHzV1cBC837xZCpwLlX0hrrxnvIg=
|
github.com/blevesearch/bleve/v2 v2.3.2/go.mod h1:96+xE5pZUOsr3Y4vHzV1cBC837xZCpwLlX0hrrxnvIg=
|
||||||
github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
||||||
|
@ -62,6 +62,10 @@ type Block struct {
|
|||||||
// The board id that the block belongs to
|
// The board id that the block belongs to
|
||||||
// required: true
|
// required: true
|
||||||
BoardID string `json:"boardId"`
|
BoardID string `json:"boardId"`
|
||||||
|
|
||||||
|
// Indicates if the card is limited
|
||||||
|
// required: false
|
||||||
|
Limited bool `json:"limited,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockPatch is a patch for modify blocks
|
// BlockPatch is a patch for modify blocks
|
||||||
@ -210,3 +214,33 @@ func StampModificationMetadata(userID string, blocks []Block, auditRec *audit.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b Block) ShouldBeLimited(cardLimitTimestamp int64) bool {
|
||||||
|
return b.Type == TypeCard &&
|
||||||
|
b.UpdateAt < cardLimitTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a limited version of the block that doesn't contain the
|
||||||
|
// contents of the block, only its IDs and type.
|
||||||
|
func (b Block) GetLimited() Block {
|
||||||
|
newBlock := Block{
|
||||||
|
Title: b.Title,
|
||||||
|
ID: b.ID,
|
||||||
|
ParentID: b.ParentID,
|
||||||
|
Schema: b.Schema,
|
||||||
|
Type: b.Type,
|
||||||
|
CreateAt: b.CreateAt,
|
||||||
|
UpdateAt: b.UpdateAt,
|
||||||
|
DeleteAt: b.DeleteAt,
|
||||||
|
WorkspaceID: b.WorkspaceID,
|
||||||
|
Limited: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if iconField, ok := b.Fields["icon"]; ok {
|
||||||
|
newBlock.Fields = map[string]interface{}{
|
||||||
|
"icon": iconField,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBlock
|
||||||
|
}
|
||||||
|
27
server/model/cloud.go
Normal file
27
server/model/cloud.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
const LimitUnlimited = 0
|
||||||
|
|
||||||
|
// BoardsCloudLimits is the representation of the limits for the
|
||||||
|
// Boards server
|
||||||
|
// swagger:model
|
||||||
|
type BoardsCloudLimits struct {
|
||||||
|
// The maximum number of cards on the server
|
||||||
|
// required: true
|
||||||
|
Cards int `json:"cards"`
|
||||||
|
|
||||||
|
// The current number of cards on the server
|
||||||
|
// required: true
|
||||||
|
UsedCards int `json:"used_cards"`
|
||||||
|
|
||||||
|
// The updated_at timestamp of the limit card
|
||||||
|
// required: true
|
||||||
|
CardLimitTimestamp int64 `json:"card_limit_timestamp"`
|
||||||
|
|
||||||
|
// The maximum number of views for each board
|
||||||
|
// required: true
|
||||||
|
Views int `json:"views"`
|
||||||
|
}
|
@ -4,10 +4,15 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
apierrors "github.com/mattermost/mattermost-plugin-api/errors"
|
apierrors "github.com/mattermost/mattermost-plugin-api/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrBlocksFromDifferentBoards is an error type that can be returned
|
||||||
|
// when a set of blocks belong to different boards.
|
||||||
|
var ErrBlocksFromDifferentBoards = errors.New("blocks belong to different boards")
|
||||||
|
|
||||||
// ErrNotFound is an error type that can be returned by store APIs when a query unexpectedly fetches no records.
|
// ErrNotFound is an error type that can be returned by store APIs when a query unexpectedly fetches no records.
|
||||||
type ErrNotFound struct {
|
type ErrNotFound struct {
|
||||||
resource string
|
resource string
|
||||||
@ -47,3 +52,30 @@ func IsErrNotFound(err error) bool {
|
|||||||
// check if this is a plugin API error
|
// check if this is a plugin API error
|
||||||
return errors.Is(err, apierrors.ErrNotFound)
|
return errors.Is(err, apierrors.ErrNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrNotAllFound is an error type that can be returned by store APIs
|
||||||
|
// when a query that should fetch a certain amount of records
|
||||||
|
// unexpectedly fetches less.
|
||||||
|
type ErrNotAllFound struct {
|
||||||
|
resources []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrNotAllFound(resources []string) *ErrNotAllFound {
|
||||||
|
return &ErrNotAllFound{
|
||||||
|
resources: resources,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (na *ErrNotAllFound) Error() string {
|
||||||
|
return fmt.Sprintf("not all instances in {%s} found", strings.Join(na.resources, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrNotAllFound returns true if `err` is or wraps a ErrNotAllFound.
|
||||||
|
func IsErrNotAllFound(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var na *ErrNotAllFound
|
||||||
|
return errors.As(err, &na)
|
||||||
|
}
|
||||||
|
@ -9,7 +9,10 @@ import (
|
|||||||
"github.com/mattermost/focalboard/server/services/store"
|
"github.com/mattermost/focalboard/server/services/store"
|
||||||
"github.com/mattermost/focalboard/server/ws"
|
"github.com/mattermost/focalboard/server/ws"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
|
|
||||||
|
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Params struct {
|
type Params struct {
|
||||||
@ -21,6 +24,8 @@ type Params struct {
|
|||||||
WSAdapter ws.Adapter
|
WSAdapter ws.Adapter
|
||||||
NotifyBackends []notify.Backend
|
NotifyBackends []notify.Backend
|
||||||
PermissionsService permissions.PermissionsService
|
PermissionsService permissions.PermissionsService
|
||||||
|
PluginAPI plugin.API
|
||||||
|
Client *pluginapi.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Params) CheckValid() error {
|
func (p Params) CheckValid() error {
|
||||||
|
@ -33,9 +33,8 @@ import (
|
|||||||
"github.com/mattermost/focalboard/server/ws"
|
"github.com/mattermost/focalboard/server/ws"
|
||||||
"github.com/oklog/run"
|
"github.com/oklog/run"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -138,6 +137,7 @@ func New(params Params) (*Server, error) {
|
|||||||
Notifications: notificationService,
|
Notifications: notificationService,
|
||||||
Logger: params.Logger,
|
Logger: params.Logger,
|
||||||
Permissions: params.PermissionsService,
|
Permissions: params.PermissionsService,
|
||||||
|
PluginAPI: params.PluginAPI,
|
||||||
SkipTemplateInit: utils.IsRunningUnitTests(),
|
SkipTemplateInit: utils.IsRunningUnitTests(),
|
||||||
}
|
}
|
||||||
app := app.New(params.Cfg, wsAdapter, appServices)
|
app := app.New(params.Cfg, wsAdapter, appServices)
|
||||||
|
@ -465,3 +465,7 @@ func (s *MattermostAuthLayer) SaveFileInfo(fileInfo *mmModel.FileInfo) error {
|
|||||||
func (s *MattermostAuthLayer) GetLicense() *mmModel.License {
|
func (s *MattermostAuthLayer) GetLicense() *mmModel.License {
|
||||||
return s.pluginAPI.GetLicense()
|
return s.pluginAPI.GetLicense()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MattermostAuthLayer) GetCloudLimits() (*mmModel.ProductLimits, error) {
|
||||||
|
return s.pluginAPI.GetCloudLimits()
|
||||||
|
}
|
||||||
|
@ -399,6 +399,21 @@ func (mr *MockStoreMockRecorder) GetBlockHistoryDescendants(arg0, arg1 interface
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBlocksByIDs mocks base method.
|
||||||
|
func (m *MockStore) GetBlocksByIDs(arg0 []string) ([]model.Block, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetBlocksByIDs", arg0)
|
||||||
|
ret0, _ := ret[0].([]model.Block)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlocksByIDs indicates an expected call of GetBlocksByIDs.
|
||||||
|
func (mr *MockStoreMockRecorder) GetBlocksByIDs(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksByIDs", reflect.TypeOf((*MockStore)(nil).GetBlocksByIDs), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// GetBlocksForBoard mocks base method.
|
// GetBlocksForBoard mocks base method.
|
||||||
func (m *MockStore) GetBlocksForBoard(arg0 string) ([]model.Block, error) {
|
func (m *MockStore) GetBlocksForBoard(arg0 string) ([]model.Block, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -566,6 +581,21 @@ func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1 interface{})
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCardLimitTimestamp mocks base method.
|
||||||
|
func (m *MockStore) GetCardLimitTimestamp() (int64, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetCardLimitTimestamp")
|
||||||
|
ret0, _ := ret[0].(int64)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCardLimitTimestamp indicates an expected call of GetCardLimitTimestamp.
|
||||||
|
func (mr *MockStoreMockRecorder) GetCardLimitTimestamp() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCardLimitTimestamp", reflect.TypeOf((*MockStore)(nil).GetCardLimitTimestamp))
|
||||||
|
}
|
||||||
|
|
||||||
// GetCategory mocks base method.
|
// GetCategory mocks base method.
|
||||||
func (m *MockStore) GetCategory(arg0 string) (*model.Category, error) {
|
func (m *MockStore) GetCategory(arg0 string) (*model.Category, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -581,6 +611,21 @@ func (mr *MockStoreMockRecorder) GetCategory(arg0 interface{}) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCloudLimits mocks base method.
|
||||||
|
func (m *MockStore) GetCloudLimits() (*model0.ProductLimits, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetCloudLimits")
|
||||||
|
ret0, _ := ret[0].(*model0.ProductLimits)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCloudLimits indicates an expected call of GetCloudLimits.
|
||||||
|
func (mr *MockStoreMockRecorder) GetCloudLimits() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockStore)(nil).GetCloudLimits))
|
||||||
|
}
|
||||||
|
|
||||||
// GetFileInfo mocks base method.
|
// GetFileInfo mocks base method.
|
||||||
func (m *MockStore) GetFileInfo(arg0 string) (*model0.FileInfo, error) {
|
func (m *MockStore) GetFileInfo(arg0 string) (*model0.FileInfo, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -895,6 +940,21 @@ func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0, arg1 interface{}) *gomo
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsedCardsCount mocks base method.
|
||||||
|
func (m *MockStore) GetUsedCardsCount() (int, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetUsedCardsCount")
|
||||||
|
ret0, _ := ret[0].(int)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsedCardsCount indicates an expected call of GetUsedCardsCount.
|
||||||
|
func (mr *MockStoreMockRecorder) GetUsedCardsCount() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsedCardsCount", reflect.TypeOf((*MockStore)(nil).GetUsedCardsCount))
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserByEmail mocks base method.
|
// GetUserByEmail mocks base method.
|
||||||
func (m *MockStore) GetUserByEmail(arg0 string) (*model.User, error) {
|
func (m *MockStore) GetUserByEmail(arg0 string) (*model.User, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -1259,6 +1319,21 @@ func (mr *MockStoreMockRecorder) UndeleteBoard(arg0, arg1 interface{}) *gomock.C
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndeleteBoard", reflect.TypeOf((*MockStore)(nil).UndeleteBoard), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndeleteBoard", reflect.TypeOf((*MockStore)(nil).UndeleteBoard), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateCardLimitTimestamp mocks base method.
|
||||||
|
func (m *MockStore) UpdateCardLimitTimestamp(arg0 int) (int64, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateCardLimitTimestamp", arg0)
|
||||||
|
ret0, _ := ret[0].(int64)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCardLimitTimestamp indicates an expected call of UpdateCardLimitTimestamp.
|
||||||
|
func (mr *MockStoreMockRecorder) UpdateCardLimitTimestamp(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCardLimitTimestamp", reflect.TypeOf((*MockStore)(nil).UpdateCardLimitTimestamp), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateCategory mocks base method.
|
// UpdateCategory mocks base method.
|
||||||
func (m *MockStore) UpdateCategory(arg0 model.Category) error {
|
func (m *MockStore) UpdateCategory(arg0 model.Category) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -100,6 +100,32 @@ func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID strin
|
|||||||
return s.blocksFromRows(rows)
|
return s.blocksFromRows(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]model.Block, error) {
|
||||||
|
query := s.getQueryBuilder(db).
|
||||||
|
Select(s.blockFields()...).
|
||||||
|
From(s.tablePrefix + "blocks").
|
||||||
|
Where(sq.Eq{"id": ids})
|
||||||
|
|
||||||
|
rows, err := query.Query()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error(`GetBlocksByIDs ERROR`, mlog.Err(err))
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer s.CloseRows(rows)
|
||||||
|
|
||||||
|
blocks, err := s.blocksFromRows(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(blocks) != len(ids) {
|
||||||
|
return nil, model.NewErrNotAllFound(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) getBlocksWithBoardID(db sq.BaseRunner, boardID string) ([]model.Block, error) {
|
func (s *SQLStore) getBlocksWithBoardID(db sq.BaseRunner, boardID string) ([]model.Block, error) {
|
||||||
query := s.getQueryBuilder(db).
|
query := s.getQueryBuilder(db).
|
||||||
Select(s.blockFields()...).
|
Select(s.blockFields()...).
|
||||||
|
117
server/services/store/sqlstore/cloud.go
Normal file
117
server/services/store/sqlstore/cloud.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package sqlstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
"github.com/mattermost/focalboard/server/services/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidCardLimitValue = errors.New("card limit value is invalid")
|
||||||
|
|
||||||
|
// activeCardsQuery applies the necessary filters to the query for it
|
||||||
|
// to fetch an active cards window if the cardLimit is set, or all the
|
||||||
|
// active cards if it's 0.
|
||||||
|
func (s *SQLStore) activeCardsQuery(builder sq.StatementBuilderType, selectStr string, cardLimit int) sq.SelectBuilder {
|
||||||
|
query := builder.
|
||||||
|
Select(selectStr).
|
||||||
|
From(s.tablePrefix + "blocks b").
|
||||||
|
Join(s.tablePrefix + "boards bd on b.board_id=bd.id").
|
||||||
|
Where(sq.Eq{
|
||||||
|
"b.delete_at": 0,
|
||||||
|
"b.type": model.TypeCard,
|
||||||
|
"bd.is_template": false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if cardLimit != 0 {
|
||||||
|
query = query.
|
||||||
|
Limit(1).
|
||||||
|
Offset(uint64(cardLimit - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUsedCardsCount returns the amount of active cards in the server.
|
||||||
|
func (s *SQLStore) getUsedCardsCount(db sq.BaseRunner) (int, error) {
|
||||||
|
row := s.activeCardsQuery(s.getQueryBuilder(db), "count(b.id)", 0).
|
||||||
|
QueryRow()
|
||||||
|
|
||||||
|
var usedCards int
|
||||||
|
err := row.Scan(&usedCards)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return usedCards, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCardLimitTimestamp returns the timestamp value from the
|
||||||
|
// system_settings table or zero if it doesn't exist.
|
||||||
|
func (s *SQLStore) getCardLimitTimestamp(db sq.BaseRunner) (int64, error) {
|
||||||
|
scanner := s.getQueryBuilder(db).
|
||||||
|
Select("value").
|
||||||
|
From(s.tablePrefix + "system_settings").
|
||||||
|
Where(sq.Eq{"id": store.CardLimitTimestampSystemKey}).
|
||||||
|
QueryRow()
|
||||||
|
|
||||||
|
var result string
|
||||||
|
err := scanner.Scan(&result)
|
||||||
|
if errors.Is(sql.ErrNoRows, err) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := strconv.Atoi(result)
|
||||||
|
if err != nil {
|
||||||
|
return 0, ErrInvalidCardLimitValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(cardLimitTimestamp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCardLimitTimestamp updates the card limit value in the
|
||||||
|
// system_settings table with the timestamp of the nth last updated
|
||||||
|
// card, being nth the value of the cardLimit parameter. If cardLimit
|
||||||
|
// is zero, the timestamp will be set to zero.
|
||||||
|
func (s *SQLStore) updateCardLimitTimestamp(db sq.BaseRunner, cardLimit int) (int64, error) {
|
||||||
|
query := s.getQueryBuilder(db).
|
||||||
|
Insert(s.tablePrefix+"system_settings").
|
||||||
|
Columns("id", "value")
|
||||||
|
|
||||||
|
var value interface{} = 0
|
||||||
|
if cardLimit != 0 {
|
||||||
|
value = s.activeCardsQuery(sq.StatementBuilder, "b.update_at", cardLimit).
|
||||||
|
OrderBy("b.update_at DESC").
|
||||||
|
Prefix("COALESCE((").Suffix("), 0)")
|
||||||
|
}
|
||||||
|
query = query.Values(store.CardLimitTimestampSystemKey, value)
|
||||||
|
|
||||||
|
if s.dbType == model.MysqlDBType {
|
||||||
|
query = query.Suffix("ON DUPLICATE KEY UPDATE value = ?", value)
|
||||||
|
} else {
|
||||||
|
query = query.Suffix(
|
||||||
|
`ON CONFLICT (id)
|
||||||
|
DO UPDATE SET value = EXCLUDED.value`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := query.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := result.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getCardLimitTimestamp(db)
|
||||||
|
}
|
@ -294,6 +294,11 @@ func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBl
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) GetBlocksByIDs(ids []string) ([]model.Block, error) {
|
||||||
|
return s.getBlocksByIDs(s.db, ids)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) GetBlocksForBoard(boardID string) ([]model.Block, error) {
|
func (s *SQLStore) GetBlocksForBoard(boardID string) ([]model.Block, error) {
|
||||||
return s.getBlocksForBoard(s.db, boardID)
|
return s.getBlocksForBoard(s.db, boardID)
|
||||||
|
|
||||||
@ -349,11 +354,21 @@ func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*mod
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) GetCardLimitTimestamp() (int64, error) {
|
||||||
|
return s.getCardLimitTimestamp(s.db)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) GetCategory(id string) (*model.Category, error) {
|
func (s *SQLStore) GetCategory(id string) (*model.Category, error) {
|
||||||
return s.getCategory(s.db, id)
|
return s.getCategory(s.db, id)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) GetCloudLimits() (*mmModel.ProductLimits, error) {
|
||||||
|
return s.getCloudLimits(s.db)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) GetFileInfo(id string) (*mmModel.FileInfo, error) {
|
func (s *SQLStore) GetFileInfo(id string) (*mmModel.FileInfo, error) {
|
||||||
return s.getFileInfo(s.db, id)
|
return s.getFileInfo(s.db, id)
|
||||||
|
|
||||||
@ -459,6 +474,11 @@ func (s *SQLStore) GetTemplateBoards(teamID string, userID string) ([]*model.Boa
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) GetUsedCardsCount() (int, error) {
|
||||||
|
return s.getUsedCardsCount(s.db)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) GetUserByEmail(email string) (*model.User, error) {
|
func (s *SQLStore) GetUserByEmail(email string) (*model.User, error) {
|
||||||
return s.getUserByEmail(s.db, email)
|
return s.getUserByEmail(s.db, email)
|
||||||
|
|
||||||
@ -769,6 +789,11 @@ func (s *SQLStore) UndeleteBoard(boardID string, modifiedBy string) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) UpdateCardLimitTimestamp(cardLimit int) (int64, error) {
|
||||||
|
return s.updateCardLimitTimestamp(s.db, cardLimit)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) UpdateCategory(category model.Category) error {
|
func (s *SQLStore) UpdateCategory(category model.Category) error {
|
||||||
return s.updateCategory(s.db, category)
|
return s.updateCategory(s.db, category)
|
||||||
|
|
||||||
|
@ -122,3 +122,7 @@ func (s *SQLStore) escapeField(fieldName string) string {
|
|||||||
func (s *SQLStore) getLicense(db sq.BaseRunner) *mmModel.License {
|
func (s *SQLStore) getLicense(db sq.BaseRunner) *mmModel.License {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) getCloudLimits(db sq.BaseRunner) (*mmModel.ProductLimits, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
@ -21,5 +21,6 @@ func TestSQLStore(t *testing.T) {
|
|||||||
t.Run("SubscriptionStore", func(t *testing.T) { storetests.StoreTestSubscriptionsStore(t, SetupTests) })
|
t.Run("SubscriptionStore", func(t *testing.T) { storetests.StoreTestSubscriptionsStore(t, SetupTests) })
|
||||||
t.Run("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(t, SetupTests) })
|
t.Run("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(t, SetupTests) })
|
||||||
t.Run("DataRetention", func(t *testing.T) { storetests.StoreTestDataRetention(t, SetupTests) })
|
t.Run("DataRetention", func(t *testing.T) { storetests.StoreTestDataRetention(t, SetupTests) })
|
||||||
|
t.Run("CloudStore", func(t *testing.T) { storetests.StoreTestCloudStore(t, SetupTests) })
|
||||||
t.Run("StoreTestFileStore", func(t *testing.T) { storetests.StoreTestFileStore(t, SetupTests) })
|
t.Run("StoreTestFileStore", func(t *testing.T) { storetests.StoreTestFileStore(t, SetupTests) })
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,10 @@ func (s *SQLStore) IsErrNotFound(err error) bool {
|
|||||||
return model.IsErrNotFound(err)
|
return model.IsErrNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) IsErrNotAllFound(err error) bool {
|
||||||
|
return model.IsErrNotAllFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) MarshalJSONB(data interface{}) ([]byte, error) {
|
func (s *SQLStore) MarshalJSONB(data interface{}) ([]byte, error) {
|
||||||
b, err := json.Marshal(data)
|
b, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -10,10 +10,13 @@ import (
|
|||||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CardLimitTimestampSystemKey = "card_limit_timestamp"
|
||||||
|
|
||||||
// Store represents the abstraction of the data storage.
|
// Store represents the abstraction of the data storage.
|
||||||
type Store interface {
|
type Store interface {
|
||||||
GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]model.Block, error)
|
GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]model.Block, error)
|
||||||
GetBlocksWithParent(boardID, parentID string) ([]model.Block, error)
|
GetBlocksWithParent(boardID, parentID string) ([]model.Block, error)
|
||||||
|
GetBlocksByIDs(ids []string) ([]model.Block, error)
|
||||||
GetBlocksWithBoardID(boardID string) ([]model.Block, error)
|
GetBlocksWithBoardID(boardID string) ([]model.Block, error)
|
||||||
GetBlocksWithType(boardID, blockType string) ([]model.Block, error)
|
GetBlocksWithType(boardID, blockType string) ([]model.Block, error)
|
||||||
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
|
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
|
||||||
@ -139,7 +142,12 @@ type Store interface {
|
|||||||
// @withTransaction
|
// @withTransaction
|
||||||
RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error)
|
RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error)
|
||||||
|
|
||||||
|
GetUsedCardsCount() (int, error)
|
||||||
|
GetCardLimitTimestamp() (int64, error)
|
||||||
|
UpdateCardLimitTimestamp(cardLimit int) (int64, error)
|
||||||
|
|
||||||
DBType() string
|
DBType() string
|
||||||
|
|
||||||
GetLicense() *mmModel.License
|
GetLicense() *mmModel.License
|
||||||
|
GetCloudLimits() (*mmModel.ProductLimits, error)
|
||||||
}
|
}
|
||||||
|
331
server/services/store/storetests/cloud.go
Normal file
331
server/services/store/storetests/cloud.go
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package storetests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
storeservice "github.com/mattermost/focalboard/server/services/store"
|
||||||
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StoreTestCloudStore(t *testing.T, setup func(t *testing.T) (storeservice.Store, func())) {
|
||||||
|
t.Run("GetUsedCardsCount", func(t *testing.T) {
|
||||||
|
store, tearDown := setup(t)
|
||||||
|
defer tearDown()
|
||||||
|
testGetUsedCardsCount(t, store)
|
||||||
|
})
|
||||||
|
t.Run("TestGetCardLimitTimestamp", func(t *testing.T) {
|
||||||
|
store, tearDown := setup(t)
|
||||||
|
defer tearDown()
|
||||||
|
testGetCardLimitTimestamp(t, store)
|
||||||
|
})
|
||||||
|
t.Run("TestUpdateCardLimitTimestamp", func(t *testing.T) {
|
||||||
|
store, tearDown := setup(t)
|
||||||
|
defer tearDown()
|
||||||
|
testUpdateCardLimitTimestamp(t, store)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetUsedCardsCount(t *testing.T, store storeservice.Store) {
|
||||||
|
userID := "user-id"
|
||||||
|
|
||||||
|
t.Run("should return zero when no cards have been created", func(t *testing.T) {
|
||||||
|
count, err := store.GetUsedCardsCount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should correctly return the cards of all boards", func(t *testing.T) {
|
||||||
|
// two boards
|
||||||
|
for _, boardID := range []string{"board1", "board2"} {
|
||||||
|
boardType := model.BoardTypeOpen
|
||||||
|
if boardID == "board2" {
|
||||||
|
boardType = model.BoardTypePrivate
|
||||||
|
}
|
||||||
|
|
||||||
|
board := &model.Board{
|
||||||
|
ID: boardID,
|
||||||
|
TeamID: testTeamID,
|
||||||
|
Type: boardType,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := store.InsertBoard(board, userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// board 1 has three cards
|
||||||
|
for _, cardID := range []string{"card1", "card2", "card3"} {
|
||||||
|
card := model.Block{
|
||||||
|
ID: cardID,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&card, userID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// board 2 has two cards
|
||||||
|
for _, cardID := range []string{"card4", "card5"} {
|
||||||
|
card := model.Block{
|
||||||
|
ID: cardID,
|
||||||
|
ParentID: "board2",
|
||||||
|
BoardID: "board2",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&card, userID))
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := store.GetUsedCardsCount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not take into account content blocks", func(t *testing.T) {
|
||||||
|
// we add a couple of content blocks
|
||||||
|
text := model.Block{
|
||||||
|
ID: "text-id",
|
||||||
|
ParentID: "card1",
|
||||||
|
BoardID: "board1",
|
||||||
|
Type: model.TypeText,
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&text, userID))
|
||||||
|
|
||||||
|
view := model.Block{
|
||||||
|
ID: "view-id",
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
Type: model.TypeView,
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&view, userID))
|
||||||
|
|
||||||
|
// and count should not change
|
||||||
|
count, err := store.GetUsedCardsCount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not take into account cards belonging to templates", func(t *testing.T) {
|
||||||
|
// we add a template with cards
|
||||||
|
templateID := "template-id"
|
||||||
|
boardTemplate := model.Block{
|
||||||
|
ID: templateID,
|
||||||
|
BoardID: templateID,
|
||||||
|
Type: model.TypeBoard,
|
||||||
|
Fields: map[string]interface{}{
|
||||||
|
"isTemplate": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&boardTemplate, userID))
|
||||||
|
|
||||||
|
for _, cardID := range []string{"card6", "card7", "card8"} {
|
||||||
|
card := model.Block{
|
||||||
|
ID: cardID,
|
||||||
|
ParentID: templateID,
|
||||||
|
BoardID: templateID,
|
||||||
|
Type: model.TypeCard,
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&card, userID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// and count should still be the same
|
||||||
|
count, err := store.GetUsedCardsCount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not take into account deleted cards", func(t *testing.T) {
|
||||||
|
// we create a ninth card on the first board
|
||||||
|
card9 := model.Block{
|
||||||
|
ID: "card9",
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
DeleteAt: utils.GetMillis(),
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&card9, userID))
|
||||||
|
|
||||||
|
// and count should still be the same
|
||||||
|
count, err := store.GetUsedCardsCount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not take into account cards from deleted boards", func(t *testing.T) {
|
||||||
|
require.NoError(t, store.DeleteBoard("board2", "user-id"))
|
||||||
|
|
||||||
|
count, err := store.GetUsedCardsCount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 3, count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetCardLimitTimestamp(t *testing.T, store storeservice.Store) {
|
||||||
|
t.Run("should return 0 if there is no entry in the database", func(t *testing.T) {
|
||||||
|
rawValue, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "", rawValue)
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return an int64 representation of the value", func(t *testing.T) {
|
||||||
|
require.NoError(t, store.SetSystemSetting(storeservice.CardLimitTimestampSystemKey, "1234"))
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1234), cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return an invalid value error if the value is not a number", func(t *testing.T) {
|
||||||
|
require.NoError(t, store.SetSystemSetting(storeservice.CardLimitTimestampSystemKey, "abc"))
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||||
|
require.ErrorContains(t, err, "card limit value is invalid")
|
||||||
|
require.Zero(t, cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateCardLimitTimestamp(t *testing.T, store storeservice.Store) {
|
||||||
|
userID := "user-id"
|
||||||
|
|
||||||
|
// two boards
|
||||||
|
for _, boardID := range []string{"board1", "board2"} {
|
||||||
|
boardType := model.BoardTypeOpen
|
||||||
|
if boardID == "board2" {
|
||||||
|
boardType = model.BoardTypePrivate
|
||||||
|
}
|
||||||
|
|
||||||
|
board := &model.Board{
|
||||||
|
ID: boardID,
|
||||||
|
TeamID: testTeamID,
|
||||||
|
Type: boardType,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := store.InsertBoard(board, userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// board 1 has five cards
|
||||||
|
for _, cardID := range []string{"card1", "card2", "card3", "card4", "card5"} {
|
||||||
|
card := model.Block{
|
||||||
|
ID: cardID,
|
||||||
|
ParentID: "board1",
|
||||||
|
BoardID: "board1",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&card, userID))
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// board 2 has five cards
|
||||||
|
for _, cardID := range []string{"card6", "card7", "card8", "card9", "card10"} {
|
||||||
|
card := model.Block{
|
||||||
|
ID: cardID,
|
||||||
|
ParentID: "board2",
|
||||||
|
BoardID: "board2",
|
||||||
|
Type: model.TypeCard,
|
||||||
|
}
|
||||||
|
require.NoError(t, store.InsertBlock(&card, userID))
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("should set the timestamp to zero if the card limit is zero", func(t *testing.T) {
|
||||||
|
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, cardLimitTimestamp)
|
||||||
|
|
||||||
|
cardLimitTimestampStr, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "0", cardLimitTimestampStr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should correctly modify the limit several times in a row", func(t *testing.T) {
|
||||||
|
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, cardLimitTimestamp)
|
||||||
|
|
||||||
|
cardLimitTimestamp, err = store.UpdateCardLimitTimestamp(10)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotZero(t, cardLimitTimestamp)
|
||||||
|
|
||||||
|
cardLimitTimestampStr, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, "0", cardLimitTimestampStr)
|
||||||
|
|
||||||
|
cardLimitTimestamp, err = store.UpdateCardLimitTimestamp(0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, cardLimitTimestamp)
|
||||||
|
|
||||||
|
cardLimitTimestampStr, err = store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "0", cardLimitTimestampStr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should set the correct timestamp", func(t *testing.T) {
|
||||||
|
t.Run("limit 10", func(t *testing.T) {
|
||||||
|
// we fetch the first block
|
||||||
|
card1, err := store.GetBlock("card1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// and assert that if the limit is 10, the stored
|
||||||
|
// timestamp corresponds to the card's update_at
|
||||||
|
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(10)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, card1.UpdateAt, cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("limit 5", func(t *testing.T) {
|
||||||
|
// if the limit is 5, the timestamp should be the one from
|
||||||
|
// the sixth card (the first five are older and out of the
|
||||||
|
card6, err := store.GetBlock("card6")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(5)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, card6.UpdateAt, cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("limit should be zero if we have less cards than the limit", func(t *testing.T) {
|
||||||
|
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("we update the first inserted card and assert that with limit 1 that's the limit that is set", func(t *testing.T) {
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
card1, err := store.GetBlock("card1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
card1.Title = "New title"
|
||||||
|
require.NoError(t, store.InsertBlock(card1, userID))
|
||||||
|
|
||||||
|
newCard1, err := store.GetBlock("card1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, newCard1.UpdateAt, cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("limit should stop applying if we remove the last card", func(t *testing.T) {
|
||||||
|
initialCardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotZero(t, initialCardLimitTimestamp)
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
require.NoError(t, store.DeleteBlock("card1", userID))
|
||||||
|
|
||||||
|
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(10)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, cardLimitTimestamp)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -5,7 +5,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IDType byte
|
type IDType byte
|
||||||
@ -27,22 +27,22 @@ const (
|
|||||||
// with the padding stripped off, and a one character alpha prefix indicating the
|
// with the padding stripped off, and a one character alpha prefix indicating the
|
||||||
// type of entity or a `7` if unknown type.
|
// type of entity or a `7` if unknown type.
|
||||||
func NewID(idType IDType) string {
|
func NewID(idType IDType) string {
|
||||||
return string(idType) + mm_model.NewId()
|
return string(idType) + mmModel.NewId()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMillis is a convenience method to get milliseconds since epoch.
|
// GetMillis is a convenience method to get milliseconds since epoch.
|
||||||
func GetMillis() int64 {
|
func GetMillis() int64 {
|
||||||
return mm_model.GetMillis()
|
return mmModel.GetMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
|
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
|
||||||
func GetMillisForTime(thisTime time.Time) int64 {
|
func GetMillisForTime(thisTime time.Time) int64 {
|
||||||
return mm_model.GetMillisForTime(thisTime)
|
return mmModel.GetMillisForTime(thisTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
|
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
|
||||||
func GetTimeForMillis(millis int64) time.Time {
|
func GetTimeForMillis(millis int64) time.Time {
|
||||||
return mm_model.GetTimeForMillis(millis)
|
return mmModel.GetTimeForMillis(millis)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecondsToMillis is a convenience method to convert seconds to milliseconds.
|
// SecondsToMillis is a convenience method to convert seconds to milliseconds.
|
||||||
@ -95,3 +95,10 @@ func Intersection(x ...[]interface{}) []interface{} {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsCloudLicense(license *mmModel.License) bool {
|
||||||
|
return license != nil &&
|
||||||
|
license.Features != nil &&
|
||||||
|
license.Features.Cloud != nil &&
|
||||||
|
*license.Features.Cloud
|
||||||
|
}
|
||||||
|
@ -6,19 +6,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
websocketActionAuth = "AUTH"
|
websocketActionAuth = "AUTH"
|
||||||
websocketActionSubscribeTeam = "SUBSCRIBE_TEAM"
|
websocketActionSubscribeTeam = "SUBSCRIBE_TEAM"
|
||||||
websocketActionUnsubscribeTeam = "UNSUBSCRIBE_TEAM"
|
websocketActionUnsubscribeTeam = "UNSUBSCRIBE_TEAM"
|
||||||
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
||||||
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
||||||
websocketActionUpdateBoard = "UPDATE_BOARD"
|
websocketActionUpdateBoard = "UPDATE_BOARD"
|
||||||
websocketActionUpdateMember = "UPDATE_MEMBER"
|
websocketActionUpdateMember = "UPDATE_MEMBER"
|
||||||
websocketActionDeleteMember = "DELETE_MEMBER"
|
websocketActionDeleteMember = "DELETE_MEMBER"
|
||||||
websocketActionUpdateBlock = "UPDATE_BLOCK"
|
websocketActionUpdateBlock = "UPDATE_BLOCK"
|
||||||
websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG"
|
websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG"
|
||||||
websocketActionUpdateCategory = "UPDATE_CATEGORY"
|
websocketActionUpdateCategory = "UPDATE_CATEGORY"
|
||||||
websocketActionUpdateCategoryBoard = "UPDATE_BOARD_CATEGORY"
|
websocketActionUpdateCategoryBoard = "UPDATE_BOARD_CATEGORY"
|
||||||
websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION"
|
websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION"
|
||||||
|
websocketActionUpdateCardLimitTimestamp = "UPDATE_CARD_LIMIT_TIMESTAMP"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
@ -36,5 +37,6 @@ type Adapter interface {
|
|||||||
BroadcastConfigChange(clientConfig model.ClientConfig)
|
BroadcastConfigChange(clientConfig model.ClientConfig)
|
||||||
BroadcastCategoryChange(category model.Category)
|
BroadcastCategoryChange(category model.Category)
|
||||||
BroadcastCategoryBoardChange(teamID, userID string, blockCategory model.BoardCategoryWebsocketData)
|
BroadcastCategoryBoardChange(teamID, userID string, blockCategory model.BoardCategoryWebsocketData)
|
||||||
|
BroadcastCardLimitTimestampChange(cardLimitTimestamp int64)
|
||||||
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,18 @@ type UpdateSubscription struct {
|
|||||||
Subscription *model.Subscription `json:"subscription"`
|
Subscription *model.Subscription `json:"subscription"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateClientConfig is sent on block updates.
|
||||||
|
type UpdateClientConfig struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
ClientConfig model.ClientConfig `json:"clientconfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateClientConfig is sent on block updates.
|
||||||
|
type UpdateCardLimitTimestamp struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
// WebsocketCommand is an incoming command from the client.
|
// WebsocketCommand is an incoming command from the client.
|
||||||
type WebsocketCommand struct {
|
type WebsocketCommand struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
|
@ -30,6 +30,7 @@ type PluginAdapterInterface interface {
|
|||||||
BroadcastBlockChange(teamID string, block model.Block)
|
BroadcastBlockChange(teamID string, block model.Block)
|
||||||
BroadcastBlockDelete(teamID, blockID, parentID string)
|
BroadcastBlockDelete(teamID, blockID, parentID string)
|
||||||
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
||||||
|
BroadcastCardLimitTimestampChange(cardLimitTimestamp int64)
|
||||||
HandleClusterEvent(ev mmModel.PluginClusterEvent)
|
HandleClusterEvent(ev mmModel.PluginClusterEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,3 +594,16 @@ func (pa *PluginAdapter) BroadcastSubscriptionChange(teamID string, subscription
|
|||||||
|
|
||||||
pa.sendTeamMessage(websocketActionUpdateSubscription, teamID, utils.StructToMap(message))
|
pa.sendTeamMessage(websocketActionUpdateSubscription, teamID, utils.StructToMap(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pa *PluginAdapter) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
|
||||||
|
pa.logger.Debug("BroadcastCardLimitTimestampChange",
|
||||||
|
mlog.Int64("cardLimitTimestamp", cardLimitTimestamp),
|
||||||
|
)
|
||||||
|
|
||||||
|
message := UpdateCardLimitTimestamp{
|
||||||
|
Action: websocketActionUpdateCardLimitTimestamp,
|
||||||
|
Timestamp: cardLimitTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
pa.sendMessageToAll(websocketActionUpdateCardLimitTimestamp, utils.StructToMap(message))
|
||||||
|
}
|
||||||
|
@ -55,12 +55,6 @@ type Server struct {
|
|||||||
store Store
|
store Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateClientConfig is sent on block updates.
|
|
||||||
type UpdateClientConfig struct {
|
|
||||||
Action string `json:"action"`
|
|
||||||
ClientConfig model.ClientConfig `json:"clientconfig"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type websocketSession struct {
|
type websocketSession struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
userID string
|
userID string
|
||||||
@ -754,3 +748,7 @@ func (ws *Server) BroadcastMemberDelete(teamID, boardID, userID string) {
|
|||||||
func (ws *Server) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) {
|
func (ws *Server) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) {
|
||||||
// not implemented for standalone server.
|
// not implemented for standalone server.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ws *Server) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
|
||||||
|
// not implemented for standalone server.
|
||||||
|
}
|
||||||
|
@ -7,7 +7,6 @@ import wsClient, {WSClient} from '../wsclient'
|
|||||||
|
|
||||||
export default function useCardListener(onChange: (blocks: Block[]) => void, onReconnect: () => void): void {
|
export default function useCardListener(onChange: (blocks: Block[]) => void, onReconnect: () => void): void {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ToDo: does this onChange need boards as well??
|
|
||||||
const onChangeHandler = (_: WSClient, blocks: Block[]) => onChange(blocks)
|
const onChangeHandler = (_: WSClient, blocks: Block[]) => onChange(blocks)
|
||||||
wsClient.addOnChange(onChangeHandler, 'block')
|
wsClient.addOnChange(onChangeHandler, 'block')
|
||||||
wsClient.addOnReconnect(onReconnect)
|
wsClient.addOnReconnect(onReconnect)
|
||||||
|
@ -141,7 +141,6 @@ class OctoClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToDo: document
|
|
||||||
private teamPath(teamId?: string): string {
|
private teamPath(teamId?: string): string {
|
||||||
let teamIdToUse = teamId
|
let teamIdToUse = teamId
|
||||||
if (!teamId) {
|
if (!teamId) {
|
||||||
@ -618,14 +617,6 @@ class OctoClient {
|
|||||||
return this.getBoardsWithPath(path)
|
return this.getBoardsWithPath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boards
|
|
||||||
// ToDo: .
|
|
||||||
// - goal? make the interface show boards & blocks for boards
|
|
||||||
// - teams (maybe current team)? boards, members, user roles in the store, whatever that is
|
|
||||||
// - selectors for boards, current team, board members
|
|
||||||
// - ops to add/delete a board, add/delete board members, change roles? .
|
|
||||||
// - WS definition and implementation
|
|
||||||
|
|
||||||
async getBoards(): Promise<Board[]> {
|
async getBoards(): Promise<Board[]> {
|
||||||
const path = this.teamPath() + '/boards'
|
const path = this.teamPath() + '/boards'
|
||||||
return this.getBoardsWithPath(path)
|
return this.getBoardsWithPath(path)
|
||||||
|
@ -6,30 +6,6 @@ import {OctoUtils} from './octoUtils'
|
|||||||
|
|
||||||
import {TestBlockFactory} from './test/testBlockFactory'
|
import {TestBlockFactory} from './test/testBlockFactory'
|
||||||
|
|
||||||
// ToDo: we need a way to duplicate the board first creating a new
|
|
||||||
// board and then dupliating and inserting all its blocks
|
|
||||||
// test('duplicateBlockTree: Board', async () => {
|
|
||||||
// const [blocks, board] = createBoardTree()
|
|
||||||
//
|
|
||||||
// const [newBlocks, newBoard, idMap] = OctoUtils.duplicateBlockTree(blocks, board.id)
|
|
||||||
//
|
|
||||||
// expect(newBlocks.length).toBe(blocks.length)
|
|
||||||
// expect(newSourceBlock.id).not.toBe(sourceBlock)
|
|
||||||
// expect(newSourceBlock.type).toBe(sourceBlock.type)
|
|
||||||
//
|
|
||||||
// // When duplicating a root block, the boardId should be re-mapped
|
|
||||||
// expect(newSourceBlock.boardId).not.toBe(sourceBlock.boardId)
|
|
||||||
// expect(idMap[sourceBlock.id]).toBe(newSourceBlock.id)
|
|
||||||
//
|
|
||||||
// for (const newBlock of newBlocks) {
|
|
||||||
// expect(newBlock.boardId).toBe(newSourceBlock.id)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for (const textBlock of newBlocks.filter((o) => o.type === 'card')) {
|
|
||||||
// expect(textBlock.parentId).toBe(newSourceBlock.id)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
test('duplicateBlockTree: Card', async () => {
|
test('duplicateBlockTree: Card', async () => {
|
||||||
const [blocks, sourceBlock] = createCardTree()
|
const [blocks, sourceBlock] = createCardTree()
|
||||||
|
|
||||||
@ -52,27 +28,6 @@ test('duplicateBlockTree: Card', async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// function createBoardTree(): [Block[], Board] {
|
|
||||||
// const blocks: Block[] = []
|
|
||||||
|
|
||||||
// const board = TestBlockFactory.createBoard()
|
|
||||||
// board.id = 'board1'
|
|
||||||
|
|
||||||
// for (let i = 0; i < 5; i++) {
|
|
||||||
// const card = TestBlockFactory.createCard(board)
|
|
||||||
// card.id = `card${i}`
|
|
||||||
// blocks.push(card)
|
|
||||||
|
|
||||||
// for (let j = 0; j < 3; j++) {
|
|
||||||
// const textBlock = TestBlockFactory.createText(card)
|
|
||||||
// textBlock.id = `text${j}`
|
|
||||||
// blocks.push(textBlock)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return [blocks, board]
|
|
||||||
// }
|
|
||||||
|
|
||||||
function createCardTree(): [Block[], Block] {
|
function createCardTree(): [Block[], Block] {
|
||||||
const blocks: Block[] = []
|
const blocks: Block[] = []
|
||||||
|
|
||||||
|
@ -56,10 +56,6 @@ const WebsocketConnection = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const incrementalBlockUpdate = (_: WSClient, blocks: Block[]) => {
|
const incrementalBlockUpdate = (_: WSClient, blocks: Block[]) => {
|
||||||
// ToDo: update this
|
|
||||||
// - create a selector to get user boards
|
|
||||||
// - replace the teamId check of blocks by a "is in my boards" check
|
|
||||||
/* const teamBlocks = blocks.filter((b: Block) => b.teamId === '0' || b.boardId in userBoardIds) */
|
|
||||||
const teamBlocks = blocks
|
const teamBlocks = blocks
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
|
@ -10,8 +10,6 @@ import {Constants} from "../constants"
|
|||||||
|
|
||||||
import {RootState} from './index'
|
import {RootState} from './index'
|
||||||
|
|
||||||
// ToDo: move this to team templates or simply templates
|
|
||||||
|
|
||||||
export const fetchGlobalTemplates = createAsyncThunk(
|
export const fetchGlobalTemplates = createAsyncThunk(
|
||||||
'globalTemplates/fetch',
|
'globalTemplates/fetch',
|
||||||
async () => {
|
async () => {
|
||||||
|
@ -27,6 +27,7 @@ export type WSMessage = {
|
|||||||
error?: string
|
error?: string
|
||||||
teamId?: string
|
teamId?: string
|
||||||
member?: BoardMember
|
member?: BoardMember
|
||||||
|
timestamp?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACTION_UPDATE_BOARD = 'UPDATE_BOARD'
|
export const ACTION_UPDATE_BOARD = 'UPDATE_BOARD'
|
||||||
@ -42,6 +43,7 @@ export const ACTION_UPDATE_CLIENT_CONFIG = 'UPDATE_CLIENT_CONFIG'
|
|||||||
export const ACTION_UPDATE_CATEGORY = 'UPDATE_CATEGORY'
|
export const ACTION_UPDATE_CATEGORY = 'UPDATE_CATEGORY'
|
||||||
export const ACTION_UPDATE_BOARD_CATEGORY = 'UPDATE_BOARD_CATEGORY'
|
export const ACTION_UPDATE_BOARD_CATEGORY = 'UPDATE_BOARD_CATEGORY'
|
||||||
export const ACTION_UPDATE_SUBSCRIPTION = 'UPDATE_SUBSCRIPTION'
|
export const ACTION_UPDATE_SUBSCRIPTION = 'UPDATE_SUBSCRIPTION'
|
||||||
|
export const ACTION_UPDATE_CARD_LIMIT_TIMESTAMP = 'UPDATE_CARD_LIMIT_TIMESTAMP'
|
||||||
|
|
||||||
type WSSubscriptionMsg = {
|
type WSSubscriptionMsg = {
|
||||||
action?: string
|
action?: string
|
||||||
@ -74,6 +76,7 @@ type OnReconnectHandler = (client: WSClient) => void
|
|||||||
type OnStateChangeHandler = (client: WSClient, state: 'init' | 'open' | 'close') => void
|
type OnStateChangeHandler = (client: WSClient, state: 'init' | 'open' | 'close') => void
|
||||||
type OnErrorHandler = (client: WSClient, e: Event) => void
|
type OnErrorHandler = (client: WSClient, e: Event) => void
|
||||||
type OnConfigChangeHandler = (client: WSClient, clientConfig: ClientConfig) => void
|
type OnConfigChangeHandler = (client: WSClient, clientConfig: ClientConfig) => void
|
||||||
|
type OnCardLimitTimestampChangeHandler = (client: WSClient, timestamp: number) => void
|
||||||
type FollowChangeHandler = (client: WSClient, subscription: Subscription) => void
|
type FollowChangeHandler = (client: WSClient, subscription: Subscription) => void
|
||||||
|
|
||||||
export type ChangeHandlerType = 'block' | 'category' | 'blockCategories' | 'board' | 'boardMembers'
|
export type ChangeHandlerType = 'block' | 'category' | 'blockCategories' | 'board' | 'boardMembers'
|
||||||
@ -110,6 +113,7 @@ class WSClient {
|
|||||||
onChange: ChangeHandlers = {Block: [], Category: [], BoardCategory: [], Board: [], BoardMember: []}
|
onChange: ChangeHandlers = {Block: [], Category: [], BoardCategory: [], Board: [], BoardMember: []}
|
||||||
onError: OnErrorHandler[] = []
|
onError: OnErrorHandler[] = []
|
||||||
onConfigChange: OnConfigChangeHandler[] = []
|
onConfigChange: OnConfigChangeHandler[] = []
|
||||||
|
onCardLimitTimestampChange: OnCardLimitTimestampChangeHandler[] = []
|
||||||
onFollowBlock: FollowChangeHandler = () => {}
|
onFollowBlock: FollowChangeHandler = () => {}
|
||||||
onUnfollowBlock: FollowChangeHandler = () => {}
|
onUnfollowBlock: FollowChangeHandler = () => {}
|
||||||
private notificationDelay = 100
|
private notificationDelay = 100
|
||||||
@ -254,6 +258,17 @@ class WSClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOnCardLimitTimestampChange(handler: OnCardLimitTimestampChangeHandler): void {
|
||||||
|
this.onCardLimitTimestampChange.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOnCardLimitTimestampChange(handler: OnCardLimitTimestampChangeHandler): void {
|
||||||
|
const index = this.onCardLimitTimestampChange.indexOf(handler)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.onCardLimitTimestampChange.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open(): void {
|
open(): void {
|
||||||
if (this.client !== null) {
|
if (this.client !== null) {
|
||||||
// configure the Mattermost websocket client callbacks
|
// configure the Mattermost websocket client callbacks
|
||||||
@ -431,6 +446,12 @@ class WSClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCardLimitTimestampHandler(action: {action: string, timestamp: number}): void {
|
||||||
|
for (const handler of this.onCardLimitTimestampChange) {
|
||||||
|
handler(this, action.timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateSubscriptionHandler(message: WSSubscriptionMsg): void {
|
updateSubscriptionHandler(message: WSSubscriptionMsg): void {
|
||||||
Utils.log('updateSubscriptionHandler: ' + message.action + '; blockId=' + message.subscription?.blockId)
|
Utils.log('updateSubscriptionHandler: ' + message.action + '; blockId=' + message.subscription?.blockId)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user