diff --git a/.vscode/launch.json b/.vscode/launch.json index 68455592b..bd338fe63 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,6 +9,7 @@ "type": "go", "request": "launch", "mode": "debug", + "buildFlags": "-tags 'json1'", "program": "${workspaceFolder}/server/main", "cwd": "${workspaceFolder}" }, diff --git a/Makefile b/Makefile index d96d3553d..bd426f76d 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ ifeq ($(BUILD_NUMBER),) BUILD_DATE := n/a endif +BUILD_TAGS += json1 + LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildNumber=$(BUILD_NUMBER)" LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildDate=$(BUILD_DATE)" LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildHash=$(BUILD_HASH)" @@ -35,25 +37,25 @@ ci: server-test server: ## Build server for local environment. $(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=dev") - cd server; go build -ldflags '$(LDFLAGS)' -o ../bin/focalboard-server ./main + cd server; go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/focalboard-server ./main server-mac: ## Build server for Mac. mkdir -p bin/mac $(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=mac") - cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -o ../bin/mac/focalboard-server ./main + cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/mac/focalboard-server ./main server-linux: ## Build server for Linux. mkdir -p bin/linux $(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux") - cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/linux/focalboard-server ./main + cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main server-win: ## Build server for Windows. $(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win") - cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/win/focalboard-server.exe ./main + cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/win/focalboard-server.exe ./main server-dll: ## Build server as Windows DLL. $(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win") - cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main + cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main server-linux-package: server-linux webapp rm -rf package @@ -83,7 +85,6 @@ server-linux-package-docker: generate: ## Install and run code generators. cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen - cd server; go get -modfile=go.tools.mod github.com/jteeuwen/go-bindata cd server; go generate ./... server-lint: ## Run linters on server code. @@ -101,18 +102,18 @@ modd-precheck: fi; \ watch: modd-precheck ## Run both server and webapp watching for changes - modd + env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd watch-single-user: modd-precheck ## Run both server and webapp in single user mode watching for changes - env FOCALBOARDSERVER_ARGS=--single-user modd + env FOCALBOARDSERVER_ARGS=--single-user FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd watch-server-test: modd-precheck ## Run server tests watching for changes - modd -f modd-servertest.conf + env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-servertest.conf server-test: server-test-sqlite server-test-mysql server-test-postgres ## Run server tests server-test-sqlite: ## Run server tests using sqlite - cd server; go test -race -v -count=1 ./... + cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./... server-test-mysql: export FB_UNIT_TESTING=1 server-test-mysql: export FB_STORE_TEST_DB_TYPE=mysql @@ -120,8 +121,9 @@ server-test-mysql: export FB_STORE_TEST_DOCKER_PORT=44445 server-test-mysql: ## Run server tests using mysql @echo Starting docker container for mysql + docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans docker-compose -f ./docker-testing/docker-compose-mysql.yml run start_dependencies - cd server; go test -race -v -count=1 ./... + cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./... docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans server-test-postgres: export FB_UNIT_TESTING=1 @@ -130,8 +132,9 @@ server-test-postgres: export FB_STORE_TEST_DOCKER_PORT=44446 server-test-postgres: ## Run server tests using postgres @echo Starting docker container for postgres + docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans docker-compose -f ./docker-testing/docker-compose-postgres.yml run start_dependencies - cd server; go test -race -v -count=1 ./... + cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./... docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans webapp: ## Build webapp. @@ -141,7 +144,7 @@ webapp-test: ## jest tests for webapp cd webapp; npm run test watch-plugin: modd-precheck ## Run and upload the plugin to a development server - modd -f modd-watchplugin.conf + env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-watchplugin.conf live-watch-plugin: modd-precheck ## Run and update locally the plugin in the development server cd mattermost-plugin; make live-watch diff --git a/config.json b/config.json index 0b806822f..7501c830e 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,8 @@ { "serverRoot": "http://localhost:8000", - "port": 8000, - "dbtype": "sqlite3", - "dbconfig": "./focalboard.db", + "port": 8000, + "dbtype": "sqlite3", + "dbconfig": "./focalboard.db", "dbtableprefix": "", "postgres_dbconfig": "dbname=focalboard sslmode=disable", "test_dbconfig": "file::memory:?cache=shared", @@ -21,5 +21,5 @@ "authMode": "native", "logging_cfg_file": "", "audit_cfg_file": "", - "enablepublicsharedboards": false + "enablePublicSharedBoards": false } diff --git a/experiments/webext/src/utils/networking.ts b/experiments/webext/src/utils/networking.ts index 7edc97623..18cfa7c33 100644 --- a/experiments/webext/src/utils/networking.ts +++ b/experiments/webext/src/utils/networking.ts @@ -38,7 +38,7 @@ export async function logIn(host: string, username: string, password: string) { export async function getBoards(host: string, token: string) { const json = await request('GET', host, 'workspaces/0/blocks?type=board', null, token) as Board[] - return json.filter(board => !board.fields.isTemplate) + return json.filter(board => !board.isTemplate) } export async function findUrlPropertyId(host: string, token: string, boardId: string) { diff --git a/import/asana/importAsana.ts b/import/asana/importAsana.ts index 806c1f5f9..66636b8d3 100644 --- a/import/asana/importAsana.ts +++ b/import/asana/importAsana.ts @@ -129,7 +129,7 @@ function convert(input: Asana): Block[] { type: 'select', options } - board.fields.cardProperties = [cardProperty] + board.cardProperties = [cardProperty] blocks.push(board) // Board view diff --git a/import/jira/jiraImporter.ts b/import/jira/jiraImporter.ts index afca95171..19140f96a 100644 --- a/import/jira/jiraImporter.ts +++ b/import/jira/jiraImporter.ts @@ -90,25 +90,25 @@ function convert(items: any[]) { board.title = 'Jira import' // Compile standard properties - board.fields.cardProperties = [] + board.cardProperties = [] const priorityProperty = buildCardPropertyFromValues('Priority', items.map(o => o.priority?._)) - board.fields.cardProperties.push(priorityProperty) + board.cardProperties.push(priorityProperty) const statusProperty = buildCardPropertyFromValues('Status', items.map(o => o.status?._)) - board.fields.cardProperties.push(statusProperty) + board.cardProperties.push(statusProperty) const resolutionProperty = buildCardPropertyFromValues('Resolution', items.map(o => o.resolution?._)) - board.fields.cardProperties.push(resolutionProperty) + board.cardProperties.push(resolutionProperty) const typeProperty = buildCardPropertyFromValues('Type', items.map(o => o.type?._)) - board.fields.cardProperties.push(typeProperty) + board.cardProperties.push(typeProperty) const assigneeProperty = buildCardPropertyFromValues('Assignee', items.map(o => o.assignee?._)) - board.fields.cardProperties.push(assigneeProperty) + board.cardProperties.push(assigneeProperty) const reporterProperty = buildCardPropertyFromValues('Reporter', items.map(o => o.reporter?._)) - board.fields.cardProperties.push(reporterProperty) + board.cardProperties.push(reporterProperty) const originalUrlProperty: IPropertyTemplate = { id: Utils.createGuid(), @@ -116,7 +116,7 @@ function convert(items: any[]) { type: 'url', options: [] } - board.fields.cardProperties.push(originalUrlProperty) + board.cardProperties.push(originalUrlProperty) const createdDateProperty: IPropertyTemplate = { id: Utils.createGuid(), @@ -124,7 +124,7 @@ function convert(items: any[]) { type: 'date', options: [] } - board.fields.cardProperties.push(createdDateProperty) + board.cardProperties.push(createdDateProperty) blocks.push(board) @@ -240,4 +240,4 @@ function showHelp() { exit(1) } -export { run } \ No newline at end of file +export { run } diff --git a/import/notion/importNotion.ts b/import/notion/importNotion.ts index 10a71e887..361c84848 100644 --- a/import/notion/importNotion.ts +++ b/import/notion/importNotion.ts @@ -135,7 +135,7 @@ function convert(input: any[], title: string): Block[] { type: 'select', options: [] } - board.fields.cardProperties.push(cardProperty) + board.cardProperties.push(cardProperty) }) // Set all column types to select @@ -177,7 +177,7 @@ function convert(input: any[], title: string): Block[] { continue } - const cardProperty = board.fields.cardProperties.find((o) => o.name === key)! + const cardProperty = board.cardProperties.find((o) => o.name === key)! let option = cardProperty.options.find((o) => o.value === value) if (!option) { const color = optionColors[optionColorIndex % optionColors.length] diff --git a/import/todoist/importTodoist.ts b/import/todoist/importTodoist.ts index a881b59bb..047d2a14b 100644 --- a/import/todoist/importTodoist.ts +++ b/import/todoist/importTodoist.ts @@ -82,7 +82,7 @@ function convert(input: Todoist, project: Project): Block[] { console.log(`Board: ${project.name}`) board.rootId = board.id board.title = project.name - board.fields.description = project.name + board.description = project.name // Convert lists (columns) to a Select property const optionIdMap = new Map() @@ -114,7 +114,7 @@ function convert(input: Todoist, project: Project): Block[] { type: 'select', options } - board.fields.cardProperties = [cardProperty] + board.cardProperties = [cardProperty] blocks.push(board) // Board view diff --git a/import/trello/importTrello.ts b/import/trello/importTrello.ts index afa95cba3..eacae528a 100644 --- a/import/trello/importTrello.ts +++ b/import/trello/importTrello.ts @@ -68,7 +68,7 @@ function convert(input: Trello): Block[] { console.log(`Board: ${input.name}`) board.rootId = board.id board.title = input.name - board.fields.description = input.desc + board.description = input.desc // Convert lists (columns) to a Select property const optionIdMap = new Map() @@ -92,7 +92,7 @@ function convert(input: Trello): Block[] { type: 'select', options } - board.fields.cardProperties = [cardProperty] + board.cardProperties = [cardProperty] blocks.push(board) // Board view diff --git a/linux/main.go b/linux/main.go index 54d0fca3b..716c19c72 100644 --- a/linux/main.go +++ b/linux/main.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/mattermost/focalboard/server/server" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/permissions/localpermissions" "github.com/webview/webview" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -66,6 +67,8 @@ func runServer(port int) (*server.Server, error) { return nil, err } + permissionsService := localpermissions.New(db, logger) + params := server.Params{ Cfg: config, SingleUserToken: sessionToken, @@ -74,6 +77,7 @@ func runServer(port int) (*server.Server, error) { ServerID: "", WSAdapter: nil, NotifyBackends: nil, + PermissionsService: permissionsService, } server, err := server.New(params) diff --git a/mattermost-plugin/go.sum b/mattermost-plugin/go.sum index 0fc337ae9..b9ba0ae14 100644 --- a/mattermost-plugin/go.sum +++ b/mattermost-plugin/go.sum @@ -61,7 +61,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOv github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-storage-blob-go v0.13.0/go.mod h1:pA9kNqtjUeQF2zOSu4s//nUdBD+e64lEuc4sVnuOfNs= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -91,7 +90,6 @@ github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/ github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk= @@ -235,7 +233,6 @@ github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkb github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -260,6 +257,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1 github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -272,20 +270,15 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ= -github.com/dhui/dktest v0.3.4 h1:VbUEcaSP+U2/yUr9d2JhSThXYEnDlGabRSHe2rIE46E= github.com/dhui/dktest v0.3.4/go.mod h1:4m4n6lmXlmVfESth7mzdcv8nBI5mOb5UROPqjM02csU= github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible h1:nhVo1udYfMj0Jsw0lnqrTjjf33aLpdgW9Wve9fHVzhQ= github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= @@ -315,6 +308,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -410,10 +404,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0= -github.com/golang-migrate/migrate/v4 v4.15.0 h1:LKvQ+CgezLw0zuR/ib1y9sQStG0vepWaEVUsQof0bo0= github.com/golang-migrate/migrate/v4 v4.15.0/go.mod h1:g9qbiDvB47WyrRnNu2t2gMZFNHKnatsYRxsGZbCi4EM= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -542,7 +534,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -557,7 +548,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= @@ -684,6 +674,7 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -778,6 +769,8 @@ github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08= github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3 h1:+E2WOqMrCGZTGjhmWVsszj2Qqx7Amh/OBUedkLLtnP4= github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3/go.mod h1:VH26NcOr3xgkSBAvh4q+9+RoBD/M9gYO2F1PISq9KMw= +github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14= +github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -807,6 +800,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -852,7 +846,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= @@ -905,9 +898,7 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= @@ -990,6 +981,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= @@ -1233,7 +1225,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -1321,6 +1312,7 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1512,9 +1504,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed h1:E159xujlywdAeN3FqsTBPzRKGUq/pDHolXbuttkC36E= golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1612,6 +1606,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1830,31 +1825,147 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= +modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM= +modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= +modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= +modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc= +modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8= +modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= +modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q= +modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= +modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ= +modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y= modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= +modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw= +modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk= +modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/mattermost-plugin/server/plugin.go b/mattermost-plugin/server/plugin.go index 21a8b837d..39febf012 100644 --- a/mattermost-plugin/server/plugin.go +++ b/mattermost-plugin/server/plugin.go @@ -14,6 +14,7 @@ import ( "github.com/mattermost/focalboard/server/server" "github.com/mattermost/focalboard/server/services/config" "github.com/mattermost/focalboard/server/services/notify" + "github.com/mattermost/focalboard/server/services/permissions/mmpermissions" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/store/mattermostauthlayer" "github.com/mattermost/focalboard/server/services/store/sqlstore" @@ -29,16 +30,17 @@ import ( ) const ( - boardsFeatureFlagName = "BoardsFeatureFlags" - pluginName = "focalboard" - sharedBoardsName = "enablepublicsharedboards" + boardsFeatureFlagName = "BoardsFeatureFlags" + pluginName = "focalboard" + sharedBoardsName = "enablepublicsharedboards" + notifyFreqCardSecondsKey = "notify_freq_card_seconds" notifyFreqBoardSecondsKey = "notify_freq_board_seconds" ) type BoardsEmbed struct { OriginalPath string `json:"originalPath"` - WorkspaceID string `json:"workspaceID"` + TeamID string `json:"teamID"` ViewID string `json:"viewID"` BoardID string `json:"boardID"` CardID string `json:"cardID"` @@ -112,7 +114,9 @@ func (p *Plugin) OnActivate() error { db = layeredStore } - p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db), logger) + permissionsService := mmpermissions.New(db, p.API) + + p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db, permissionsService), db, logger) backendParams := notifyBackendParams{ cfg: cfg, @@ -137,13 +141,14 @@ func (p *Plugin) OnActivate() error { mentionsBackend.AddListener(subscriptionsBackend) params := server.Params{ - Cfg: cfg, - SingleUserToken: "", - DBStore: db, - Logger: logger, - ServerID: serverID, - WSAdapter: p.wsPluginAdapter, - NotifyBackends: notifyBackends, + Cfg: cfg, + SingleUserToken: "", + DBStore: db, + Logger: logger, + ServerID: serverID, + WSAdapter: p.wsPluginAdapter, + NotifyBackends: notifyBackends, + PermissionsService: permissionsService, } server, err := server.New(params) @@ -373,11 +378,11 @@ func postWithBoardsEmbed(post *mmModel.Post) *mmModel.Post { return post } - workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit) + teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit) - if workspaceID != "" && boardID != "" && viewID != "" && cardID != "" { + if teamID != "" && boardID != "" && viewID != "" && cardID != "" { b, _ := json.Marshal(BoardsEmbed{ - WorkspaceID: workspaceID, + TeamID: teamID, BoardID: boardID, ViewID: viewID, CardID: cardID, @@ -430,7 +435,7 @@ func getFirstLinkAndShortenAllBoardsLink(postMessage string) (firstLink, newPost return firstLink, newPostMessage } -func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardID string) { +func returnBoardsParams(pathArray []string) (teamID, boardID, viewID, cardID string) { // The reason we are doing this search for the first instance of boards or plugins is to take into account URL subpaths index := -1 for i := 0; i < len(pathArray); i++ { @@ -441,7 +446,7 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI } if index == -1 { - return workspaceID, boardID, viewID, cardID + return teamID, boardID, viewID, cardID } // If at index, the parameter in the path is boards, @@ -450,27 +455,27 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI // If at index, the parameter in the path is plugins, // then we've copied this from a shared board - // For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/workspace/workspaceID/boardID/viewID/cardID + // For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/team/teamID/boardID/viewID/cardID // For card links copied on a shared board, the path looks like - // {...Mattermost Url}.../plugins/focalboard/workspace/workspaceID/shared/boardID/viewID/cardID?r=read_token + // {...Mattermost Url}.../plugins/focalboard/team/teamID/shared/boardID/viewID/cardID?r=read_token // This is a non-shared board card link - if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "workspace" { - workspaceID = pathArray[index+2] + if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "team" { + teamID = pathArray[index+2] boardID = pathArray[index+3] viewID = pathArray[index+4] cardID = pathArray[index+5] } else if len(pathArray)-index == 8 && pathArray[index] == "plugins" && pathArray[index+1] == "focalboard" && - pathArray[index+2] == "workspace" && + pathArray[index+2] == "team" && pathArray[index+4] == "shared" { // This is a shared board card link - workspaceID = pathArray[index+3] + teamID = pathArray[index+3] boardID = pathArray[index+5] viewID = pathArray[index+6] cardID = pathArray[index+7] } - return workspaceID, boardID, viewID, cardID + return teamID, boardID, viewID, cardID } func isBoardsLink(link string) bool { @@ -489,6 +494,6 @@ func isBoardsLink(link string) bool { return false } - workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit) - return workspaceID != "" && boardID != "" && viewID != "" && cardID != "" + teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit) + return teamID != "" && boardID != "" && viewID != "" && cardID != "" } diff --git a/mattermost-plugin/webapp/src/components/boardsUnfurl/boardsUnfurl.tsx b/mattermost-plugin/webapp/src/components/boardsUnfurl/boardsUnfurl.tsx index d4292f5a2..25b75cf9a 100644 --- a/mattermost-plugin/webapp/src/components/boardsUnfurl/boardsUnfurl.tsx +++ b/mattermost-plugin/webapp/src/components/boardsUnfurl/boardsUnfurl.tsx @@ -36,17 +36,34 @@ function mapStateToProps(state: GlobalState) { } } +class FocalboardEmbeddedData { + teamID: string + cardID: string + boardID: string + readToken: string + originalPath: string + + constructor(rawData: string) { + const parsed = JSON.parse(rawData) + this.teamID = parsed.teamID || parsed.workspaceID + this.cardID = parsed.cardID + this.boardID = parsed.boardID + this.readToken = parsed.readToken + this.originalPath = parsed.originalPath + } +} + const BoardsUnfurl = (props: Props): JSX.Element => { if (!props.embed || !props.embed.data) { return <> } const {embed, locale} = props - const focalboardInformation = JSON.parse(embed.data) - const {workspaceID, cardID, boardID, readToken, originalPath} = focalboardInformation + const focalboardInformation: FocalboardEmbeddedData = new FocalboardEmbeddedData(embed.data) + const {teamID, cardID, boardID, readToken, originalPath} = focalboardInformation const baseURL = window.location.origin - if (!workspaceID || !cardID || !boardID) { + if (!teamID || !cardID || !boardID) { return <> } @@ -57,20 +74,19 @@ const BoardsUnfurl = (props: Props): JSX.Element => { useEffect(() => { const fetchData = async () => { - const [cards, boards] = await Promise.all( + const [cards, fetchedBoard] = await Promise.all( [ - octoClient.getBlocksWithBlockID(cardID, workspaceID, readToken), - octoClient.getBlocksWithBlockID(boardID, workspaceID, readToken), + octoClient.getBlocksWithBlockID(cardID, boardID, readToken), + octoClient.getBoard(boardID), ], ) const [firstCard] = cards as Card[] - const [firstBoard] = boards as Board[] - if (!firstCard || !firstBoard) { + if (!firstCard || !fetchedBoard) { setLoading(false) return null } setCard(firstCard) - setBoard(firstBoard) + setBoard(fetchedBoard) if (firstCard.fields.contentOrder.length) { let [firstContentBlockID] = firstCard.fields?.contentOrder @@ -79,7 +95,7 @@ const BoardsUnfurl = (props: Props): JSX.Element => { [firstContentBlockID] = firstContentBlockID } - const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, workspaceID, readToken) as ContentBlock[] + const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, boardID, readToken) as ContentBlock[] const [firstContentBlock] = contentBlock if (!firstContentBlock) { setLoading(false) @@ -103,8 +119,8 @@ const BoardsUnfurl = (props: Props): JSX.Element => { let totalNumberOfCheckBoxes = 0 // We will just display the first 3 or less select/multi-select properties and do a +n for remainder if any remainder - for (let i = 0; i < board.fields.cardProperties.length; i++) { - const optionInBoard = board.fields.cardProperties[i] + for (let i = 0; i < board.cardProperties.length; i++) { + const optionInBoard = board.cardProperties[i] let valueToLookUp = card.fields.properties[optionInBoard.id] // Since these are always set and not included in the card properties @@ -132,7 +148,11 @@ const BoardsUnfurl = (props: Props): JSX.Element => { continue } - propertiesToDisplay.push({optionName: optionInBoard.name, optionValue: optionSelected.value, optionValueColour: optionSelected.color}) + propertiesToDisplay.push({ + optionName: optionInBoard.name, + optionValue: optionSelected.value, + optionValueColour: optionSelected.color, + }) } remainder += (Object.keys(card.fields.properties).length - propertiesToDisplay.length - totalNumberOfCheckBoxes) html = Utils.htmlFromMarkdown(content?.title || '') diff --git a/mattermost-plugin/webapp/src/error_boundary.tsx b/mattermost-plugin/webapp/src/error_boundary.tsx index 4ad15e8ae..616c7273d 100644 --- a/mattermost-plugin/webapp/src/error_boundary.tsx +++ b/mattermost-plugin/webapp/src/error_boundary.tsx @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import React from 'react' -import PropTypes from 'prop-types' import {Utils} from '../../../webapp/src/utils' @@ -10,9 +9,12 @@ type State = { hasError: boolean } -export default class ErrorBoundary extends React.Component { +type Props = { + children: React.ReactNode +} + +export default class ErrorBoundary extends React.Component { state = {hasError: false} - propTypes = {children: PropTypes.node.isRequired} msg = 'Redirecting to error page...' handleError = (): void => { diff --git a/mattermost-plugin/webapp/src/index.tsx b/mattermost-plugin/webapp/src/index.tsx index dfe89cfba..7cb715bfa 100644 --- a/mattermost-plugin/webapp/src/index.tsx +++ b/mattermost-plugin/webapp/src/index.tsx @@ -9,18 +9,22 @@ import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/r import {GlobalState} from 'mattermost-redux/types/store' -const windowAny = (window as any) +import {selectTeam} from 'mattermost-redux/actions/teams' + +import {SuiteWindow} from '../../../webapp/src/types/index' + +const windowAny = (window as SuiteWindow) windowAny.baseURL = '/plugins/focalboard' windowAny.frontendBaseURL = '/boards' windowAny.isFocalboardPlugin = true import App from '../../../webapp/src/app' import store from '../../../webapp/src/store' +import {setTeam} from '../../../webapp/src/store/teams' import {Utils} from '../../../webapp/src/utils' import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader' import FocalboardIcon from '../../../webapp/src/widgets/icons/logo' import {setMattermostTheme} from '../../../webapp/src/theme' -import {UserSettings} from '../../../webapp/src/userSettings' import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient' @@ -30,7 +34,13 @@ import '../../../webapp/src/styles/labels.scss' import octoClient from '../../../webapp/src/octoClient' import BoardsUnfurl from './components/boardsUnfurl/boardsUnfurl' -import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK, ACTION_UPDATE_CLIENT_CONFIG, ACTION_UPDATE_SUBSCRIPTION} from './../../../webapp/src/wsclient' +import wsClient, { + MMWebSocketClient, + ACTION_UPDATE_BLOCK, + ACTION_UPDATE_CLIENT_CONFIG, + ACTION_UPDATE_SUBSCRIPTION, + ACTION_UPDATE_CATEGORY, ACTION_UPDATE_BLOCK_CATEGORY, ACTION_UPDATE_BOARD, +} from './../../../webapp/src/wsclient' import manifest from './manifest' import ErrorBoundary from './error_boundary' @@ -164,6 +174,7 @@ export default class Plugin { let theme = mmStore.getState().entities.preferences.myPreferences.theme setMattermostTheme(theme) let lastViewedChannel = mmStore.getState().entities.channels.currentChannelId + let prevTeamID: string mmStore.subscribe(() => { const currentUserId = mmStore.getState().entities.users.currentUserId const currentChannel = mmStore.getState().entities.channels.currentChannelId @@ -171,22 +182,41 @@ export default class Plugin { localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel) lastViewedChannel = currentChannel } + + // Watch for change in active team. + // This handles the user selecting a team from the team sidebar. + const currentTeamID = mmStore.getState().entities.teams.currentTeamId + if (currentTeamID && currentTeamID !== prevTeamID) { + prevTeamID = currentTeamID + store.dispatch(setTeam(currentTeamID)) + browserHistory.push(`/team/${currentTeamID}`) + wsClient.subscribeToTeam(currentTeamID) + } }) if (this.registry.registerProduct) { windowAny.frontendBaseURL = subpath + '/boards' - const goToFocalboardWorkspace = () => { - const currentChannel = mmStore.getState().entities.channels.currentChannelId - TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {workspaceID: currentChannel}) - window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener') + const goToFocalboard = () => { + const currentTeam = mmStore.getState().entities.teams.currentTeamId + TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {teamID: currentTeam}) + window.open(`${windowAny.frontendBaseURL}/team/${currentTeam}`, '_blank', 'noopener') } - this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(, goToFocalboardWorkspace, 'Boards', 'Boards') + + this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(, goToFocalboard, 'Boards', 'Boards') + this.registry.registerProduct( + '/boards', + 'product-boards', + 'Boards', + '/boards', + MainApp, + HeaderComponent, + () => null, + true, + ) const goToFocalboardTemplate = () => { const currentChannel = mmStore.getState().entities.channels.currentChannelId - TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {workspaceID: currentChannel}) - UserSettings.lastBoardId = null - UserSettings.lastViewId = null + TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {channelID: currentChannel}) window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener') } @@ -194,11 +224,9 @@ export default class Plugin { this.channelHeaderButtonId = registry.registerChannelIntroButtonAction(, goToFocalboardTemplate, 'Boards') } - this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/boards/welcome', MainApp, HeaderComponent) - if (this.registry.registerAppBarComponent) { const appBarIconURL = windowAny.baseURL + '/public/app-bar-icon.png' - this.registry.registerAppBarComponent(appBarIconURL, goToFocalboardWorkspace, 'Open Boards Workspace') + this.registry.registerAppBarComponent(appBarIconURL, goToFocalboard, 'Open Boards') } this.registry.registerPostWillRenderEmbedComponent((embed) => embed.type === 'boards', BoardsUnfurl, false) @@ -247,7 +275,9 @@ export default class Plugin { } // register websocket handlers - this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK}`, (e: any) => wsClient.updateBlockHandler(e.data)) + this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BOARD}`, (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_BLOCK_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_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data)) this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data)) @@ -267,6 +297,16 @@ export default class Plugin { } } }) + windowAny.setTeamInSidebar = (teamID: string) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + mmStore.dispatch(selectTeam(teamID)) + } + windowAny.getCurrentTeamId = (): string => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return mmStore.getState().entities.teams.currentTeamId + } } uninitialize(): void { diff --git a/mattermost-plugin/webapp/src/types/mattermost-webapp/index.d.ts b/mattermost-plugin/webapp/src/types/mattermost-webapp/index.d.ts index 9e64bb4b2..ba58df95a 100644 --- a/mattermost-plugin/webapp/src/types/mattermost-webapp/index.d.ts +++ b/mattermost-plugin/webapp/src/types/mattermost-webapp/index.d.ts @@ -10,7 +10,7 @@ export interface PluginRegistry { registerCustomRoute(route: string, component: React.ElementType) registerProductRoute(route: string, component: React.ElementType) unregisterComponent(componentId: string) - registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCompoent: React.ElementType) + registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCentreComponent: React.ElementType, headerRightComponent?: React.ElementType, showTeamSidebar: boolean) registerPostWillRenderEmbedComponent(match: (embed: {type: string, data: any}) => void, component: any, toggleable: boolean) registerWebSocketEventHandler(event: string, handler: (e: any) => void) unregisterWebSocketEventHandler(event: string) diff --git a/modd-servertest.conf b/modd-servertest.conf index 933fb66c2..1efb8e22d 100644 --- a/modd-servertest.conf +++ b/modd-servertest.conf @@ -1,3 +1,3 @@ **/*.go { - prep: cd server && go test -race -v ./... + prep: cd server && go test -tags $FOCALBOARD_BUILD_TAGS -race -v ./... } diff --git a/modd.conf b/modd.conf index 930f724cd..0b000842c 100644 --- a/modd.conf +++ b/modd.conf @@ -1,5 +1,5 @@ **/*.go !**/*_test.go { - prep: cd server && go build -o ../bin/focalboard-server ./main + prep: cd server && go build -tags $FOCALBOARD_BUILD_TAGS -o ../bin/focalboard-server ./main daemon +sigterm: ./bin/focalboard-server $FOCALBOARDSERVER_ARGS } diff --git a/server/.golangci.yml b/server/.golangci.yml index 4643e85bd..c1381759f 100644 --- a/server/.golangci.yml +++ b/server/.golangci.yml @@ -13,7 +13,7 @@ linters-settings: disable: - fieldalignment lll: - line-length: 150 + line-length: 180 dupl: threshold: 200 revive: diff --git a/server/api/api.go b/server/api/api.go index e98aded30..b833f2429 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -16,7 +17,7 @@ import ( "github.com/mattermost/focalboard/server/app" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/audit" - "github.com/mattermost/focalboard/server/services/store" + "github.com/mattermost/focalboard/server/services/permissions" "github.com/mattermost/focalboard/server/utils" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -29,8 +30,8 @@ const ( ) const ( - ErrorNoWorkspaceCode = 1000 - ErrorNoWorkspaceMessage = "No workspace" + ErrorNoTeamCode = 1000 + ErrorNoTeamMessage = "No team" ) type PermissionError struct { @@ -47,17 +48,20 @@ func (pe PermissionError) Error() string { type API struct { app *app.App authService string + permissions permissions.PermissionsService singleUserToken string MattermostAuth bool logger *mlog.Logger audit *audit.Audit } -func NewAPI(app *app.App, singleUserToken string, authService string, logger *mlog.Logger, audit *audit.Audit) *API { +func NewAPI(app *app.App, singleUserToken string, authService string, permissions permissions.PermissionsService, + logger *mlog.Logger, audit *audit.Audit) *API { return &API{ app: app, singleUserToken: singleUserToken, authService: authService, + permissions: permissions, logger: logger, audit: audit, } @@ -68,58 +72,103 @@ func (a *API) RegisterRoutes(r *mux.Router) { apiv1.Use(a.panicHandler) apiv1.Use(a.requireCSRFToken) - apiv1.HandleFunc("/workspaces/{workspaceID}/blocks", a.sessionRequired(a.handleGetBlocks)).Methods("GET") - apiv1.HandleFunc("/workspaces/{workspaceID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST") - apiv1.HandleFunc("/workspaces/{workspaceID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH") - apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE") - apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST") - apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH") - apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}/subtree", a.attachSession(a.handleGetSubTree, false)).Methods("GET") + // Board APIs + apiv1.HandleFunc("/teams/{teamID}/boards", a.sessionRequired(a.handleGetBoards)).Methods("GET") + apiv1.HandleFunc("/teams/{teamID}/boards/search", a.sessionRequired(a.handleSearchBoards)).Methods("GET") + apiv1.HandleFunc("/teams/{teamID}/templates", a.sessionRequired(a.handleGetTemplates)).Methods("GET") + apiv1.HandleFunc("/boards", a.sessionRequired(a.handleCreateBoard)).Methods("POST") + apiv1.HandleFunc("/boards/{boardID}", a.attachSession(a.handleGetBoard, false)).Methods("GET") + apiv1.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handlePatchBoard)).Methods("PATCH") + apiv1.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handleDeleteBoard)).Methods("DELETE") + apiv1.HandleFunc("/boards/{boardID}/duplicate", a.sessionRequired(a.handleDuplicateBoard)).Methods("POST") + apiv1.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handleGetBlocks)).Methods("GET") + apiv1.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST") + apiv1.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH") + apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE") + apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH") + apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST") + apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/subtree", a.attachSession(a.handleGetSubTree, false)).Methods("GET") + apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.attachSession(a.handleDuplicateBlock, false)).Methods("POST") - apiv1.HandleFunc("/workspaces/{workspaceID}/sharing/{rootID}", a.sessionRequired(a.handlePostSharing)).Methods("POST") - apiv1.HandleFunc("/workspaces/{workspaceID}/sharing/{rootID}", a.sessionRequired(a.handleGetSharing)).Methods("GET") + // Import&Export APIs + apiv1.HandleFunc("/boards/{boardID}/blocks/export", a.sessionRequired(a.handleExport)).Methods("GET") + apiv1.HandleFunc("/boards/{boardID}/blocks/import", a.sessionRequired(a.handleImport)).Methods("POST") - apiv1.HandleFunc("/workspaces/{workspaceID}", a.sessionRequired(a.handleGetWorkspace)).Methods("GET") - apiv1.HandleFunc("/workspaces/{workspaceID}/regenerate_signup_token", a.sessionRequired(a.handlePostWorkspaceRegenerateSignupToken)).Methods("POST") - apiv1.HandleFunc("/workspaces/{workspaceID}/users", a.sessionRequired(a.getWorkspaceUsers)).Methods("GET") + // Member APIs + apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET") + apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST") + apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT") + apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE") + + // Sharing APIs + apiv1.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST") + apiv1.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handleGetSharing)).Methods("GET") + + // Team APIs + apiv1.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET") + apiv1.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET") + apiv1.HandleFunc("/teams/{teamID}/regenerate_signup_token", a.sessionRequired(a.handlePostTeamRegenerateSignupToken)).Methods("POST") + apiv1.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET") + apiv1.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET") + apiv1.HandleFunc("/teams/{teamID}/{boardID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST") // User APIs apiv1.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET") + apiv1.HandleFunc("/users/me/memberships", a.sessionRequired(a.handleGetMyMemberships)).Methods("GET") apiv1.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET") apiv1.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST") apiv1.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut) + // BoardsAndBlocks APIs + apiv1.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleCreateBoardsAndBlocks)).Methods("POST") + apiv1.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handlePatchBoardsAndBlocks)).Methods("PATCH") + apiv1.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleDeleteBoardsAndBlocks)).Methods("DELETE") + + // Auth APIs apiv1.HandleFunc("/login", a.handleLogin).Methods("POST") apiv1.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST") apiv1.HandleFunc("/register", a.handleRegister).Methods("POST") apiv1.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET") - apiv1.HandleFunc("/workspaces/{workspaceID}/{rootID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST") + // Category APIs + apiv1.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleCreateCategory)).Methods(http.MethodPost) + apiv1.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleUpdateCategory)).Methods(http.MethodPut) + apiv1.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleDeleteCategory)).Methods(http.MethodDelete) - apiv1.HandleFunc("/workspaces", a.sessionRequired(a.handleGetUserWorkspaces)).Methods("GET") + // Category Block APIs + apiv1.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleGetUserCategoryBlocks)).Methods(http.MethodGet) + apiv1.HandleFunc("/teams/{teamID}/categories/{categoryID}/blocks/{blockID}", a.sessionRequired(a.handleUpdateCategoryBlock)).Methods(http.MethodPost) // Get Files API - files := r.PathPrefix("/files").Subrouter() - files.HandleFunc("/workspaces/{workspaceID}/{rootID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET") + files.HandleFunc("/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET") // Subscriptions - apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST") - apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE") - apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET") + apiv1.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST") + apiv1.HandleFunc("/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE") + apiv1.HandleFunc("/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET") // onboarding tour endpoints - apiv1.HandleFunc("/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost) + apiv1.HandleFunc("/teams/{teamID}/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost) // archives - apiv1.HandleFunc("/workspaces/{workspaceID}/archive/export", a.sessionRequired(a.handleArchiveExport)).Methods("GET") - apiv1.HandleFunc("/workspaces/{workspaceID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST") + apiv1.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET") + apiv1.HandleFunc("/boards/{boardID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST") } func (a *API) RegisterAdminRoutes(r *mux.Router) { r.HandleFunc("/api/v1/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST") } +func getUserID(r *http.Request) string { + ctx := r.Context() + session, ok := ctx.Value(sessionContextKey).(*model.Session) + if !ok { + return "" + } + return session.UserID +} + func (a *API) panicHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { @@ -165,7 +214,7 @@ func (a *API) checkCSRFToken(r *http.Request) bool { return token == HeaderRequestedWithXML } -func (a *API) hasValidReadTokenForBlock(r *http.Request, container store.Container, blockID string) bool { +func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool { query := r.URL.Query() readToken := query.Get("read_token") @@ -173,71 +222,17 @@ func (a *API) hasValidReadTokenForBlock(r *http.Request, container store.Contain return false } - isValid, err := a.app.IsValidReadToken(container, blockID, readToken) + isValid, err := a.app.IsValidReadToken(boardID, readToken) if err != nil { - a.logger.Error("IsValidReadToken ERROR", mlog.Err(err)) + a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err)) return false } return isValid } -func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID string) (*store.Container, error) { - ctx := r.Context() - session, _ := ctx.Value(sessionContextKey).(*model.Session) - - if a.MattermostAuth { - // Workspace auth - vars := mux.Vars(r) - workspaceID := vars["workspaceID"] - - container := store.Container{ - WorkspaceID: workspaceID, - } - - if workspaceID == "0" { - return &container, nil - } - - // Has session and access to workspace - if session != nil && a.app.DoesUserHaveWorkspaceAccess(session.UserID, container.WorkspaceID) { - return &container, nil - } - - // No session, but has valid read token (read-only mode) - if len(blockID) > 0 && - a.hasValidReadTokenForBlock(r, container, blockID) && - a.app.GetClientConfig().EnablePublicSharedBoards { - return &container, nil - } - - return nil, PermissionError{"access denied to workspace"} - } - - // Native auth: always use root workspace - container := store.Container{ - WorkspaceID: "0", - } - - // Has session - if session != nil { - return &container, nil - } - - // No session, but has valid read token (read-only mode) - if len(blockID) > 0 && a.hasValidReadTokenForBlock(r, container, blockID) { - return &container, nil - } - - return nil, PermissionError{"access denied to workspace"} -} - -func (a *API) getContainer(r *http.Request) (*store.Container, error) { - return a.getContainerAllowingReadTokenForBlock(r, "") -} - func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks getBlocks + // swagger:operation GET /api/v1/boards/{boardID}/blocks getBlocks // // Returns blocks // @@ -245,9 +240,9 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Board ID // required: true // type: string // - name: parent_id @@ -279,14 +274,35 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { blockType := query.Get("type") all := query.Get("all") blockID := query.Get("block_id") - container, err := a.getContainerAllowingReadTokenForBlock(r, blockID) + boardID := mux.Vars(r)["boardID"] + + userID := getUserID(r) + + board, err := a.app.GetBoard(boardID) if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "Board not found", nil) + return + } + + if board.IsTemplate { + if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"}) + return + } + } else { + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) + return + } + } auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("parentID", parentID) auditRec.AddMeta("blockType", blockType) auditRec.AddMeta("all", all) @@ -296,22 +312,27 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { var block *model.Block switch { case all != "": - blocks, err = a.app.GetAllBlocks(*container) + blocks, err = a.app.GetBlocksForBoard(boardID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } case blockID != "": - block, err = a.app.GetBlockByID(*container, blockID) + block, err = a.app.GetBlockByID(blockID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } if block != nil { + if block.BoardID != boardID { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } + blocks = append(blocks, *block) } default: - blocks, err = a.app.GetBlocks(*container, parentID, blockType) + blocks, err = a.app.GetBlocks(boardID, parentID, blockType) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -319,6 +340,7 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { } a.logger.Debug("GetBlocks", + mlog.String("boardID", boardID), mlog.String("parentID", parentID), mlog.String("blockType", blockType), mlog.String("blockID", blockID), @@ -337,8 +359,240 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { auditRec.Success() } +func stampModificationMetadata(r *http.Request, blocks []model.Block, auditRec *audit.Record) { + userID := getUserID(r) + if userID == model.SingleUser { + userID = "" + } + + now := utils.GetMillis() + for i := range blocks { + blocks[i].ModifiedBy = userID + blocks[i].UpdateAt = now + + if auditRec != nil { + auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i]) + } + } +} + +func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) { + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var category model.Category + + err = json.Unmarshal(requestBody, &category) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + auditRec := a.makeAuditRecord(r, "createCategory", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + + ctx := r.Context() + session := ctx.Value(sessionContextKey).(*model.Session) + + // user can only create category for themselves + if category.UserID != session.UserID { + a.errorResponse( + w, + r.URL.Path, + http.StatusBadRequest, + fmt.Sprintf("userID %s and category userID %s mismatch", session.UserID, category.UserID), + nil, + ) + return + } + + vars := mux.Vars(r) + teamID := vars["teamID"] + + if category.TeamID != teamID { + a.errorResponse( + w, + r.URL.Path, + http.StatusBadRequest, + "teamID mismatch", + nil, + ) + return + } + + createdCategory, err := a.app.CreateCategory(&category) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + data, err := json.Marshal(createdCategory) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, data) + auditRec.AddMeta("categoryID", createdCategory.ID) + auditRec.Success() +} + +func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + categoryID := vars["categoryID"] + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var category model.Category + err = json.Unmarshal(requestBody, &category) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + auditRec := a.makeAuditRecord(r, "updateCategory", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + + if categoryID != category.ID { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "categoryID mismatch in patch and body", nil) + return + } + + ctx := r.Context() + session := ctx.Value(sessionContextKey).(*model.Session) + + // user can only update category for themselves + if category.UserID != session.UserID { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "user ID mismatch in session and category", nil) + return + } + + teamID := vars["teamID"] + if category.TeamID != teamID { + a.errorResponse( + w, + r.URL.Path, + http.StatusBadRequest, + "teamID mismatch", + nil, + ) + return + } + + updatedCategory, err := a.app.UpdateCategory(&category) + if err != nil { + switch { + case errors.Is(err, app.ErrorCategoryDeleted): + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err) + case errors.Is(err, app.ErrorCategoryPermissionDenied): + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err) + default: + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + } + return + } + + data, err := json.Marshal(updatedCategory) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, data) + auditRec.Success() +} + +func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session := ctx.Value(sessionContextKey).(*model.Session) + vars := mux.Vars(r) + + userID := session.UserID + teamID := vars["teamID"] + categoryID := vars["categoryID"] + + auditRec := a.makeAuditRecord(r, "deleteCategory", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + + deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID) + if err != nil { + if errors.Is(err, app.ErrorCategoryPermissionDenied) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err) + } else { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + } + return + } + + data, err := json.Marshal(deletedCategory) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, data) + auditRec.Success() +} + +func (a *API) handleGetUserCategoryBlocks(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session := ctx.Value(sessionContextKey).(*model.Session) + userID := session.UserID + + vars := mux.Vars(r) + teamID := vars["teamID"] + + auditRec := a.makeAuditRecord(r, "getUserCategoryBlocks", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + + categoryBlocks, err := a.app.GetUserCategoryBlocks(userID, teamID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + data, err := json.Marshal(categoryBlocks) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, data) + auditRec.Success() +} + +func (a *API) handleUpdateCategoryBlock(w http.ResponseWriter, r *http.Request) { + auditRec := a.makeAuditRecord(r, "updateCategoryBlock", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + + vars := mux.Vars(r) + categoryID := vars["categoryID"] + blockID := vars["blockID"] + teamID := vars["teamID"] + + ctx := r.Context() + session := ctx.Value(sessionContextKey).(*model.Session) + userID := session.UserID + + err := a.app.AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, []byte("ok")) + auditRec.Success() +} + func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks updateBlocks + // swagger:operation POST /api/v1/boards/{boardID}/blocks updateBlocks // // Insert blocks. The specified IDs will only be used to link // blocks with existing ones, the rest will be replaced by server @@ -348,9 +602,9 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Board ID // required: true // type: string // - name: Body @@ -375,9 +629,13 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + boardID := mux.Vars(r)["boardID"] + userID := getUserID(r) + + // in phase 1 we use "manage_board_cards", but we would have to + // check on specific actions for phase 2 + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) return } @@ -414,6 +672,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) return } + + if block.BoardID != boardID { + message := fmt.Sprintf("invalid BoardID for block id %s", block.ID) + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) + return + } } blocks = model.GenerateBlockIDs(blocks, a.logger) @@ -423,20 +687,19 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID model.StampModificationMetadata(userID, blocks, auditRec) // this query param exists when creating template from board, or board from template sourceBoardID := r.URL.Query().Get("sourceBoardID") if sourceBoardID != "" { - if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, container.WorkspaceID, blocks); updateFileIDsErr != nil { + if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", updateFileIDsErr) return } } - newBlocks, err := a.app.InsertBlocks(*container, blocks, session.UserID, true) + newBlocks, err := a.app.InsertBlocks(blocks, session.UserID, true) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -484,7 +747,7 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) { // default: // description: internal error // schema: - // "$ref": "#/definitions/ErrorResponse + // "$ref": "#/definitions/ErrorResponse" requestBody, err := ioutil.ReadAll(r.Body) if err != nil { @@ -599,15 +862,15 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) + userID := getUserID(r) + var user *model.User var err error auditRec := a.makeAuditRecord(r, "getMe", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) - if session.UserID == model.SingleUser { + if userID == model.SingleUser { now := utils.GetMillis() user = &model.User{ ID: model.SingleUser, @@ -617,7 +880,7 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { UpdateAt: now, } } else { - user, err = a.app.GetUser(session.UserID) + user, err = a.app.GetUser(userID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -636,8 +899,53 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { auditRec.Success() } +func (a *API) handleGetMyMemberships(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/users/me/memberships getMyMemberships + // + // Returns the currently users board memberships + // + // --- + // produces: + // - application/json + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/BoardMember" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + userID := getUserID(r) + + auditRec := a.makeAuditRecord(r, "getMyBoardMemberships", audit.Fail) + auditRec.AddMeta("userID", userID) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + + members, err := a.app.GetMembersForUser(userID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + membersData, err := json.Marshal(members) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, membersData) + + auditRec.Success() +} + func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /api/v1/workspaces/{workspaceID}/blocks/{blockID} deleteBlock + // swagger:operation DELETE /api/v1/boards/{boardID}/blocks/{blockID} deleteBlock // // Deletes a block // @@ -645,9 +953,9 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Board ID // required: true // type: string // - name: blockID @@ -665,30 +973,38 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - + userID := getUserID(r) vars := mux.Vars(r) + boardID := vars["boardID"] blockID := vars["blockID"] - container, err := a.getContainer(r) + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) + return + } + + block, err := a.app.GetBlockByID(blockID) if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if block == nil || block.BoardID != boardID { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) return } auditRec := a.makeAuditRecord(r, "deleteBlock", audit.Fail) defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("blockID", blockID) - err = a.app.DeleteBlock(*container, blockID, userID) + err = a.app.DeleteBlock(blockID, userID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } - a.logger.Debug("DELETE Block", mlog.String("blockID", blockID)) + a.logger.Debug("DELETE Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID)) jsonStringResponse(w, http.StatusOK, "{}") auditRec.Success() @@ -730,17 +1046,11 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) blockID := vars["blockID"] - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) - return - } - auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail) defer a.audit.LogRecord(audit.LevelModify, auditRec) auditRec.AddMeta("blockID", blockID) - err = a.app.UndeleteBlock(*container, blockID, userID) + err := a.app.UndeleteBlock(blockID, userID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -753,7 +1063,7 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) { } func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /api/v1/workspaces/{workspaceID}/blocks/{blockID} patchBlock + // swagger:operation PATCH /api/v1/boards/{boardID}/blocks/{blockID} patchBlock // // Partially updates a block // @@ -761,9 +1071,9 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Board ID // required: true // type: string // - name: blockID @@ -787,16 +1097,23 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - + userID := getUserID(r) vars := mux.Vars(r) + boardID := vars["boardID"] blockID := vars["blockID"] - container, err := a.getContainer(r) + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) + return + } + + block, err := a.app.GetBlockByID(blockID) if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if block == nil || block.BoardID != boardID { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) return } @@ -815,22 +1132,23 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) { auditRec := a.makeAuditRecord(r, "patchBlock", audit.Fail) defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("blockID", blockID) - err = a.app.PatchBlock(*container, blockID, patch, userID) + err = a.app.PatchBlock(blockID, patch, userID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } - a.logger.Debug("PATCH Block", mlog.String("blockID", blockID)) + a.logger.Debug("PATCH Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID)) jsonStringResponse(w, http.StatusOK, "{}") auditRec.Success() } func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /api/v1/workspaces/{workspaceID}/blocks/ patchBlocks + // swagger:operation PATCH /api/v1/boards/{boardID}/blocks/ patchBlocks // // Partially updates batch of blocks // @@ -838,7 +1156,7 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path // description: Workspace ID // required: true @@ -863,11 +1181,8 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) { session := ctx.Value(sessionContextKey).(*model.Session) userID := session.UserID - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) - return - } + vars := mux.Vars(r) + teamID := vars["teamID"] requestBody, err := ioutil.ReadAll(r.Body) if err != nil { @@ -888,7 +1203,7 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), patches.BlockIDs[i]) } - err = a.app.PatchBlocks(*container, patches, userID) + err = a.app.PatchBlocks(teamID, patches, userID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -901,7 +1216,7 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) { } func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks/{blockID}/subtree getSubTree + // swagger:operation GET /api/v1/boards/{boardID}/blocks/{blockID}/subtree getSubTree // // Returns the blocks of a subtree // @@ -909,9 +1224,9 @@ func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Board ID // required: true // type: string // - name: blockID @@ -940,12 +1255,13 @@ func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" + userID := getUserID(r) vars := mux.Vars(r) + boardID := vars["boardID"] blockID := vars["blockID"] - container, err := a.getContainerAllowingReadTokenForBlock(r, blockID) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + if !a.hasValidReadTokenForBoard(r, boardID) && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) return } @@ -963,9 +1279,10 @@ func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { auditRec := a.makeAuditRecord(r, "getSubTree", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("blockID", blockID) - blocks, err := a.app.GetSubTree(*container, blockID, int(levels)) + blocks, err := a.app.GetSubTree(boardID, blockID, int(levels), model.QuerySubtreeOptions{}) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -973,6 +1290,7 @@ func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { a.logger.Debug("GetSubTree", mlog.Int64("levels", levels), + mlog.String("boardID", boardID), mlog.String("blockID", blockID), mlog.Int("block_count", len(blocks)), ) @@ -988,25 +1306,210 @@ func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { auditRec.Success() } -// Sharing - -func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v1/workspaces/{workspaceID}/sharing/{rootID} getSharing +func (a *API) handleExport(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/boards/{boardID}/blocks/export exportBlocks // - // Returns sharing information for a root block + // Returns all blocks of a board // // --- // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Board ID // required: true // type: string - // - name: rootID + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/Block" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + userID := getUserID(r) + vars := mux.Vars(r) + boardID := vars["boardID"] + + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) + return + } + + query := r.URL.Query() + rootID := query.Get("root_id") + + auditRec := a.makeAuditRecord(r, "export", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("rootID", rootID) + + var blocks []model.Block + var err error + if rootID == "" { + blocks, err = a.app.GetBlocksForBoard(boardID) + } else { + blocks, err = a.app.GetBlocksWithBoardID(boardID) + } + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("raw blocks", mlog.Int("block_count", len(blocks))) + auditRec.AddMeta("rawCount", len(blocks)) + + blocks = filterOrphanBlocks(blocks) + + a.logger.Debug("EXPORT filtered blocks", mlog.Int("block_count", len(blocks))) + auditRec.AddMeta("filteredCount", len(blocks)) + + json, err := json.Marshal(blocks) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, json) + + auditRec.Success() +} + +func filterOrphanBlocks(blocks []model.Block) (ret []model.Block) { + queue := make([]model.Block, 0) + childrenOfBlockWithID := make(map[string]*[]model.Block) + + // Build the trees from nodes + for _, block := range blocks { + if len(block.ParentID) == 0 { + // Queue root blocks to process first + queue = append(queue, block) + } else { + siblings := childrenOfBlockWithID[block.ParentID] + if siblings != nil { + *siblings = append(*siblings, block) + } else { + siblings := []model.Block{block} + childrenOfBlockWithID[block.ParentID] = &siblings + } + } + } + + // Map the trees to an array, which skips orphaned nodes + blocks = make([]model.Block, 0) + for len(queue) > 0 { + block := queue[0] + queue = queue[1:] // dequeue + blocks = append(blocks, block) + children := childrenOfBlockWithID[block.ID] + if children != nil { + queue = append(queue, *children...) + } + } + + return blocks +} + +func (a *API) handleImport(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /api/v1/boards/{boardID}/blocks/import importBlocks + // + // Import blocks on a given board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID // in: path - // description: ID of the root block + // description: Board ID + // required: true + // type: string + // - name: Body + // in: body + // description: array of blocks to import + // required: true + // schema: + // type: array + // items: + // "$ref": "#/definitions/Block" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + userID := getUserID(r) + vars := mux.Vars(r) + boardID := vars["boardID"] + + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) + return + } + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var blocks []model.Block + + err = json.Unmarshal(requestBody, &blocks) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + auditRec := a.makeAuditRecord(r, "import", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) + + // all blocks should now be part of the board that they're being + // imported onto + for i := range blocks { + blocks[i].BoardID = boardID + } + + stampModificationMetadata(r, blocks, auditRec) + + if _, err = a.app.InsertBlocks(model.GenerateBlockIDs(blocks, a.logger), userID, false); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonStringResponse(w, http.StatusOK, "{}") + + a.logger.Debug("IMPORT BlockIDs", mlog.Int("block_count", len(blocks))) + auditRec.AddMeta("blockCount", len(blocks)) + auditRec.Success() +} + +// Sharing + +func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/boards/{boardID}/sharing getSharing + // + // Returns sharing information for a board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID // required: true // type: string // security: @@ -1022,23 +1525,27 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { // "$ref": "#/definitions/ErrorResponse" vars := mux.Vars(r) - rootID := vars["rootID"] + boardID := vars["boardID"] - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + userID := getUserID(r) + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"}) return } auditRec := a.makeAuditRecord(r, "getSharing", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("rootID", rootID) + auditRec.AddMeta("boardID", boardID) - sharing, err := a.app.GetSharing(*container, rootID) + sharing, err := a.app.GetSharing(boardID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } + if sharing == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } sharingData, err := json.Marshal(sharing) if err != nil { @@ -1048,11 +1555,8 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { jsonBytesResponse(w, http.StatusOK, sharingData) - if sharing == nil { - sharing = &model.Sharing{} - } a.logger.Debug("GET sharing", - mlog.String("rootID", rootID), + mlog.String("boardID", boardID), mlog.String("shareID", sharing.ID), mlog.Bool("enabled", sharing.Enabled), ) @@ -1062,22 +1566,17 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { } func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /api/v1/workspaces/{workspaceID}/sharing/{rootID} postSharing + // swagger:operation POST /api/v1/boards/{boardID}/sharing postSharing // - // Sets sharing information for a root block + // Sets sharing information for a board // // --- // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID - // required: true - // type: string - // - name: rootID - // in: path - // description: ID of the root block + // description: Board ID // required: true // type: string // - name: Body @@ -1096,9 +1595,11 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + boardID := mux.Vars(r)["boardID"] + + userID := getUserID(r) + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"}) return } @@ -1109,21 +1610,27 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { } var sharing model.Sharing - err = json.Unmarshal(requestBody, &sharing) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } + // Stamp boardID from the URL + sharing.ID = boardID + auditRec := a.makeAuditRecord(r, "postSharing", audit.Fail) defer a.audit.LogRecord(audit.LevelModify, auditRec) auditRec.AddMeta("shareID", sharing.ID) auditRec.AddMeta("enabled", sharing.Enabled) - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID + // Stamp ModifiedBy + modifiedBy := userID + if userID == model.SingleUser { + modifiedBy = "" + } + sharing.ModifiedBy = modifiedBy + if userID == model.SingleUser { userID = "" } @@ -1139,7 +1646,7 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { sharing.ModifiedBy = userID - err = a.app.UpsertSharing(*container, sharing) + err = a.app.UpsertSharing(sharing) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1151,90 +1658,132 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { auditRec.Success() } -// Workspace +// Team -func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v1/workspaces/{workspaceID} getWorkspace +func (a *API) handleGetTeams(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/teams getTeams // - // Returns information of the root workspace + // Returns information of all the teams // // --- // produces: // - application/json - // parameters: - // - name: workspaceID - // in: path - // description: Workspace ID - // required: true - // type: string // security: // - BearerAuth: [] // responses: // '200': // description: success // schema: - // "$ref": "#/definitions/Workspace" + // type: array + // items: + // "$ref": "#/definitions/Team" // default: // description: internal error // schema: // "$ref": "#/definitions/ErrorResponse" - var workspace *model.Workspace - var err error + userID := getUserID(r) - if a.MattermostAuth { - vars := mux.Vars(r) - workspaceID := vars["workspaceID"] - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - if !a.app.DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) { - a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "user does not have workspace access", nil) - return - } - - workspace, err = a.app.GetWorkspace(workspaceID) - if err != nil { - a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) - } - if workspace == nil { - a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid workspace", nil) - return - } - } else { - workspace, err = a.app.GetRootWorkspace() - if err != nil { - a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) - return - } + teams, err := a.app.GetTeamsForUser(userID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) } - auditRec := a.makeAuditRecord(r, "getWorkspace", audit.Fail) + auditRec := a.makeAuditRecord(r, "getTeams", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("resultWorkspaceID", workspace.ID) + auditRec.AddMeta("teamCount", len(teams)) - workspaceData, err := json.Marshal(workspace) + data, err := json.Marshal(teams) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } - jsonBytesResponse(w, http.StatusOK, workspaceData) + jsonBytesResponse(w, http.StatusOK, data) auditRec.Success() } -func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /api/v1/workspaces/{workspaceID}/regenerate_signup_token regenerateSignupToken +func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/teams/{teamID} getTeam // - // Regenerates the signup token for the root workspace + // Returns information of the root team // // --- // produces: // - application/json // parameters: - // - name: workspaceID + // - name: teamID // in: path - // description: Workspace ID + // description: Team ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // "$ref": "#/definitions/Team" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + vars := mux.Vars(r) + teamID := vars["teamID"] + userID := getUserID(r) + + if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) + return + } + + var team *model.Team + var err error + + if a.MattermostAuth { + team, err = a.app.GetTeam(teamID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + } + if team == nil { + a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid team", nil) + return + } + } else { + team, err = a.app.GetRootTeam() + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + } + + auditRec := a.makeAuditRecord(r, "getTeam", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("resultTeamID", team.ID) + + data, err := json.Marshal(team) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + jsonBytesResponse(w, http.StatusOK, data) + auditRec.Success() +} + +func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /api/v1/teams/{teamID}/regenerate_signup_token regenerateSignupToken + // + // Regenerates the signup token for the root team + // + // --- + // produces: + // - application/json + // parameters: + // - name: teamID + // in: path + // description: Team ID // required: true // type: string // security: @@ -1247,7 +1796,7 @@ func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r // schema: // "$ref": "#/definitions/ErrorResponse" - workspace, err := a.app.GetRootWorkspace() + team, err := a.app.GetRootTeam() if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1256,9 +1805,9 @@ func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r auditRec := a.makeAuditRecord(r, "regenerateSignupToken", audit.Fail) defer a.audit.LogRecord(audit.LevelModify, auditRec) - workspace.SignupToken = utils.NewID(utils.IDTypeToken) + team.SignupToken = utils.NewID(utils.IDTypeToken) - err = a.app.UpsertWorkspaceSignupToken(*workspace) + err = a.app.UpsertTeamSignupToken(*team) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1271,7 +1820,7 @@ func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r // File upload func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /workspaces/{workspaceID}/{rootID}/{fileID} getFile + // swagger:operation GET /boards/{boardID}/{rootID}/{fileID} getFile // // Returns the contents of an uploaded file // @@ -1282,9 +1831,9 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { // - image/png // - image/gif // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Board ID // required: true // type: string // - name: rootID @@ -1308,20 +1857,29 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { // "$ref": "#/definitions/ErrorResponse" vars := mux.Vars(r) - workspaceID := vars["workspaceID"] - rootID := vars["rootID"] + boardID := vars["boardID"] filename := vars["filename"] + userID := getUserID(r) - // Caller must have access to the root block's container - _, err := a.getContainerAllowingReadTokenForBlock(r, rootID) + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) + return + } + + board, err := a.app.GetBoard(boardID) if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) return } auditRec := a.makeAuditRecord(r, "getFile", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("rootID", rootID) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("teamID", board.TeamID) auditRec.AddMeta("filename", filename) contentType := "image/jpg" @@ -1337,7 +1895,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", contentType) - fileReader, err := a.app.GetFileReader(workspaceID, rootID, filename) + fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1365,7 +1923,7 @@ func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) { } func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /api/v1/workspaces/{workspaceID}/{rootID}/files uploadFile + // swagger:operation POST /api/v1/teams/{teamID}/boards/{boardID}/files uploadFile // // Upload a binary file, attached to a root block // @@ -1375,14 +1933,14 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID + // - name: teamID // in: path - // description: Workspace ID + // description: ID of the team // required: true // type: string - // - name: rootID + // - name: boardID // in: path - // description: ID of the root block + // description: Board ID // required: true // type: string // - name: uploaded file @@ -1402,13 +1960,21 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { // "$ref": "#/definitions/ErrorResponse" vars := mux.Vars(r) - workspaceID := vars["workspaceID"] - rootID := vars["rootID"] + boardID := vars["boardID"] + userID := getUserID(r) - // Caller must have access to the root block's container - _, err := a.getContainerAllowingReadTokenForBlock(r, rootID) + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) + return + } + + board, err := a.app.GetBoard(boardID) if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) return } @@ -1421,10 +1987,11 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { auditRec := a.makeAuditRecord(r, "uploadFile", audit.Fail) defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("rootID", rootID) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("teamID", board.TeamID) auditRec.AddMeta("filename", handle.Filename) - fileID, err := a.app.SaveFile(file, workspaceID, rootID, handle.Filename) + fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1446,20 +2013,25 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { auditRec.Success() } -func (a *API) getWorkspaceUsers(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v1/workspaces/{workspaceID}/users getWorkspaceUsers +func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/teams/{teamID}/users getTeamUsers // - // Returns workspace users + // Returns team users // // --- // produces: // - application/json // parameters: - // - name: workspaceID + // - name: teamID // in: path - // description: Workspace ID + // description: Team ID // required: true // type: string + // - name: search + // in: query + // description: string to filter users list + // required: false + // type: string // security: // - BearerAuth: [] // responses: @@ -1475,21 +2047,22 @@ func (a *API) getWorkspaceUsers(w http.ResponseWriter, r *http.Request) { // "$ref": "#/definitions/ErrorResponse" vars := mux.Vars(r) - workspaceID := vars["workspaceID"] + teamID := vars["teamID"] + userID := getUserID(r) + query := r.URL.Query() + searchQuery := query.Get("search") - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - if !a.app.DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) { - a.errorResponse(w, r.URL.Path, http.StatusForbidden, "Access denied to workspace", PermissionError{"access denied to workspace"}) + if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "Access denied to team", PermissionError{"access denied to team"}) return } auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) - users, err := a.app.GetWorkspaceUsers(workspaceID) + users, err := a.app.SearchTeamUsers(teamID, searchQuery) if err != nil { - a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "searchQuery="+searchQuery, err) return } @@ -1505,10 +2078,140 @@ func (a *API) getWorkspaceUsers(w http.ResponseWriter, r *http.Request) { auditRec.Success() } +func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/teams/{teamID}/boards getBoards + // + // Returns team boards + // + // --- + // produces: + // - application/json + // parameters: + // - name: teamID + // in: path + // description: Team ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/Board" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + teamID := mux.Vars(r)["teamID"] + userID := getUserID(r) + + if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) + return + } + + auditRec := a.makeAuditRecord(r, "getBoards", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("teamID", teamID) + + // retrieve boards list + boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("GetBoards", + mlog.String("teamID", teamID), + mlog.Int("boardsCount", len(boards)), + ) + + data, err := json.Marshal(boards) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.AddMeta("boardsCount", len(boards)) + auditRec.Success() +} + +func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/teams/{teamID}/templates getTemplates + // + // Returns team templates + // + // --- + // produces: + // - application/json + // parameters: + // - name: teamID + // in: path + // description: Team ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/Board" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + teamID := mux.Vars(r)["teamID"] + userID := getUserID(r) + + if teamID != "0" && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) + return + } + + auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("teamID", teamID) + + // retrieve boards list + boards, err := a.app.GetTemplateBoards(teamID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("GetTemplates", + mlog.String("teamID", teamID), + mlog.Int("boardsCount", len(boards)), + ) + + data, err := json.Marshal(boards) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.AddMeta("templatesCount", len(boards)) + auditRec.Success() +} + // subscriptions func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /api/v1/workspaces/{workspaceID}/subscriptions createSubscription + // swagger:operation POST /api/v1/subscriptions createSubscription // // Creates a subscription to a block for a user. The user will receive change notifications for the block. // @@ -1516,11 +2219,6 @@ func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID - // in: path - // description: Workspace ID - // required: true - // type: string // - name: Body // in: body // description: subscription definition @@ -1539,12 +2237,6 @@ func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) - return - } - requestBody, err := ioutil.ReadAll(r.Body) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) @@ -1578,13 +2270,13 @@ func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { } // check for valid block - block, err := a.app.GetBlockByID(*container, sub.BlockID) + block, err := a.app.GetBlockByID(sub.BlockID) if err != nil || block == nil { a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid blockID", err) return } - subNew, err := a.app.CreateSubscription(*container, &sub) + subNew, err := a.app.CreateSubscription(&sub) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1606,7 +2298,7 @@ func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { } func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /api/v1/workspaces/{workspaceID}/subscriptions/{blockID}/{subscriberID} deleteSubscription + // swagger:operation DELETE /api/v1/subscriptions/{blockID}/{subscriberID} deleteSubscription // // Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block. // @@ -1614,11 +2306,6 @@ func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID - // in: path - // description: Workspace ID - // required: true - // type: string // - name: blockID // in: path // description: Block ID @@ -1646,12 +2333,6 @@ func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) { blockID := vars["blockID"] subscriberID := vars["subscriberID"] - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) - return - } - auditRec := a.makeAuditRecord(r, "deleteSubscription", audit.Fail) defer a.audit.LogRecord(audit.LevelModify, auditRec) auditRec.AddMeta("block_id", blockID) @@ -1663,7 +2344,7 @@ func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) { return } - _, err = a.app.DeleteSubscription(*container, blockID, subscriberID) + _, err := a.app.DeleteSubscription(blockID, subscriberID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1679,7 +2360,7 @@ func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) { } func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v1/workspaces/{workspaceID}/subscriptions/{subscriberID} getSubscriptions + // swagger:operation GET /api/v1/subscriptions/{subscriberID} getSubscriptions // // Gets subscriptions for a user. // @@ -1687,11 +2368,6 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: - // - name: workspaceID - // in: path - // description: Workspace ID - // required: true - // type: string // - name: subscriberID // in: path // description: Subscriber ID @@ -1716,12 +2392,6 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) subscriberID := vars["subscriberID"] - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) - return - } - auditRec := a.makeAuditRecord(r, "getSubscriptions", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) auditRec.AddMeta("subscriber_id", subscriberID) @@ -1732,7 +2402,7 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) { return } - subs, err := a.app.GetSubscriptions(*container, subscriberID) + subs, err := a.app.GetSubscriptions(subscriberID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return @@ -1748,21 +2418,115 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } - jsonBytesResponse(w, http.StatusOK, json) auditRec.AddMeta("subscription_count", len(subs)) auditRec.Success() } +func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /api/v1/boards createBoard + // + // Creates a new board + // + // --- + // produces: + // - application/json + // parameters: + // - name: Body + // in: body + // description: the board to create + // required: true + // schema: + // "$ref": "#/definitions/Board" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // $ref: '#/definitions/Board' + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + userID := getUserID(r) + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var newBoard *model.Board + if err = json.Unmarshal(requestBody, &newBoard); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + if newBoard.Type == model.BoardTypeOpen { + if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create public boards"}) + return + } + } else { + if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create private boards"}) + return + } + } + + if err = newBoard.IsValid(); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) + return + } + + auditRec := a.makeAuditRecord(r, "createBoard", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("teamID", newBoard.TeamID) + auditRec.AddMeta("boardType", newBoard.Type) + + // create board + board, err := a.app.CreateBoard(newBoard, userID, true) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("CreateBoard", + mlog.String("teamID", board.TeamID), + mlog.String("boardID", board.ID), + mlog.String("boardType", string(board.Type)), + mlog.String("userID", userID), + ) + + data, err := json.Marshal(board) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /api/v1/onboard onboard + // swagger:operation POST /api/v1/team/{teamID}/onboard onboard // // Onboards a user on Boards. // // --- // produces: // - application/json + // parameters: + // - name: teamID + // in: path + // description: Team ID + // required: true + // type: string // security: // - BearerAuth: [] // responses: @@ -1774,18 +2538,19 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) { // description: internal error // schema: // "$ref": "#/definitions/ErrorResponse" + teamID := mux.Vars(r)["teamID"] ctx := r.Context() session := ctx.Value(sessionContextKey).(*model.Session) - workspaceID, boardID, err := a.app.PrepareOnboardingTour(session.UserID) + teamID, boardID, err := a.app.PrepareOnboardingTour(session.UserID, teamID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return } response := map[string]string{ - "workspaceID": workspaceID, - "boardID": boardID, + "teamID": teamID, + "boardID": boardID, } data, err := json.Marshal(response) if err != nil { @@ -1796,6 +2561,1200 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) { jsonBytesResponse(w, http.StatusOK, data) } +func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/boards/{boardID} getBoard + // + // Returns a board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // "$ref": "#/definitions/Board" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + userID := getUserID(r) + + hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) + if userID == "" && !hasValidReadToken { + a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) + return + } + + board, err := a.app.GetBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } + + if !hasValidReadToken { + if board.Type == model.BoardTypePrivate { + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) + return + } + } else { + if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) + return + } + } + } + + auditRec := a.makeAuditRecord(r, "getBoard", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("boardID", boardID) + + a.logger.Debug("GetBoard", + mlog.String("boardID", boardID), + ) + + data, err := json.Marshal(board) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) { + // swagger:operation PATCH /api/v1/boards/{boardID} patchBoard + // + // Partially updates a board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // - name: Body + // in: body + // description: board patch to apply + // required: true + // schema: + // "$ref": "#/definitions/BoardPatch" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // $ref: '#/definitions/Board' + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + board, err := a.app.GetBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } + + userID := getUserID(r) + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var patch *model.BoardPatch + if err = json.Unmarshal(requestBody, &patch); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + if err = patch.IsValid(); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) + return + } + + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board properties"}) + return + } + + if patch.Type != nil { + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"}) + return + } + } + + auditRec := a.makeAuditRecord(r, "patchBoard", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("userID", userID) + + // patch board + updatedBoard, err := a.app.PatchBoard(patch, boardID, userID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("PatchBoard", + mlog.String("boardID", boardID), + mlog.String("userID", userID), + ) + + data, err := json.Marshal(updatedBoard) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) { + // swagger:operation DELETE /api/v1/boards/{boardID} deleteBoard + // + // Removes a board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + userID := getUserID(r) + + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"}) + return + } + + auditRec := a.makeAuditRecord(r, "deleteBoard", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) + + if err := a.app.DeleteBoard(boardID, userID); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("DELETE Board", mlog.String("boardID", boardID)) + jsonStringResponse(w, http.StatusOK, "{}") + + auditRec.Success() +} + +func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /api/v1/boards/{boardID}/duplicate duplicateBoard + // + // Returns the new created board and all the blocks + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // $ref: '#/definitions/BoardsAndBlocks' + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + userID := getUserID(r) + query := r.URL.Query() + asTemplate := query.Get("asTemplate") + toTeam := query.Get("toTeam") + + if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) + return + } + + hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) + if userID == "" && !hasValidReadToken { + a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) + return + } + + board, err := a.app.GetBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } + + if !hasValidReadToken { + if board.Type == model.BoardTypePrivate { + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) + return + } + } else { + if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) + return + } + } + } + + auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("boardID", boardID) + + a.logger.Debug("DuplicateBoard", + mlog.String("boardID", boardID), + ) + + boardsAndBlocks, _, err := a.app.DuplicateBoard(boardID, userID, toTeam, asTemplate == "true") + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) + return + } + + data, err := json.Marshal(boardsAndBlocks) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /api/v1/boards/{boardID}/blocks/{blockID}/duplicate duplicateBlock + // + // Returns the new created blocks + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // - name: blockID + // in: path + // description: Block ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/Block" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + blockID := mux.Vars(r)["blockID"] + userID := getUserID(r) + query := r.URL.Query() + asTemplate := query.Get("asTemplate") + + hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) + if userID == "" && !hasValidReadToken { + a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) + return + } + + board, err := a.app.GetBlockByID(blockID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } + + auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("blockID", blockID) + + a.logger.Debug("DuplicateBlock", + mlog.String("boardID", boardID), + mlog.String("blockID", blockID), + ) + + blocks, err := a.app.DuplicateBlock(boardID, blockID, userID, asTemplate == "true") + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) + return + } + + data, err := json.Marshal(blocks) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/teams/{teamID}/boards/search searchBoards + // + // Returns the boards that match with a search term + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // - name: teamID + // in: path + // description: Team ID + // required: true + // type: string + // - name: q + // in: query + // description: The search term. Must have at least one character + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/Board" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + teamID := mux.Vars(r)["teamID"] + term := r.URL.Query().Get("q") + userID := getUserID(r) + + if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) + return + } + + if len(term) == 0 { + jsonStringResponse(w, http.StatusOK, "[]") + return + } + + auditRec := a.makeAuditRecord(r, "searchBoards", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("teamID", teamID) + + // retrieve boards list + boards, err := a.app.SearchBoardsForUserAndTeam(term, userID, teamID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("SearchBoards", + mlog.String("teamID", teamID), + mlog.Int("boardsCount", len(boards)), + ) + + data, err := json.Marshal(boards) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.AddMeta("boardsCount", len(boards)) + auditRec.Success() +} + +func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/boards/{boardID}/members getMembersForBoard + // + // Returns the members of the board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/BoardMember" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + userID := getUserID(r) + + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board members"}) + return + } + + auditRec := a.makeAuditRecord(r, "getMembersForBoard", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) + + members, err := a.app.GetMembersForBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("GetMembersForBoard", + mlog.String("boardID", boardID), + mlog.Int("membersCount", len(members)), + ) + + data, err := json.Marshal(members) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /boards/{boardID}/members addMember + // + // Adds a new member to a board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // - name: Body + // in: body + // description: membership to replace the current one with + // required: true + // schema: + // "$ref": "#/definitions/BoardMember" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // $ref: '#/definitions/BoardMember' + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + board, err := a.app.GetBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } + + userID := getUserID(r) + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var reqBoardMember *model.BoardMember + if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + if reqBoardMember.UserID == "" { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + // currently all memberships are created as editors by default + newBoardMember := &model.BoardMember{ + UserID: reqBoardMember.UserID, + BoardID: boardID, + SchemeEditor: true, + } + + if board.Type == model.BoardTypePrivate && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) + return + } + + auditRec := a.makeAuditRecord(r, "addMember", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("addedUserID", reqBoardMember.UserID) + + member, err := a.app.AddMemberToBoard(newBoardMember) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("AddMember", + mlog.String("boardID", board.ID), + mlog.String("addedUserID", reqBoardMember.UserID), + ) + + data, err := json.Marshal(member) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) { + // swagger:operation PUT /boards/{boardID}/members/{userID} updateMember + // + // Updates a board member + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // - name: userID + // in: path + // description: User ID + // required: true + // type: string + // - name: Body + // in: body + // description: membership to replace the current one with + // required: true + // schema: + // "$ref": "#/definitions/BoardMember" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // $ref: '#/definitions/BoardMember' + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + paramsUserID := mux.Vars(r)["userID"] + userID := getUserID(r) + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var reqBoardMember *model.BoardMember + if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + newBoardMember := &model.BoardMember{ + UserID: paramsUserID, + BoardID: boardID, + SchemeAdmin: reqBoardMember.SchemeAdmin, + SchemeEditor: reqBoardMember.SchemeEditor, + SchemeCommenter: reqBoardMember.SchemeCommenter, + SchemeViewer: reqBoardMember.SchemeViewer, + } + + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) + return + } + + auditRec := a.makeAuditRecord(r, "patchMember", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("patchedUserID", paramsUserID) + + member, err := a.app.UpdateBoardMember(newBoardMember) + if errors.Is(err, app.ErrBoardMemberIsLastAdmin) { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("PatchMember", + mlog.String("boardID", boardID), + mlog.String("patchedUserID", paramsUserID), + ) + + data, err := json.Marshal(member) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) { + // swagger:operation DELETE /api/v1/boards/{boardID}/members/{userID} deleteMember + // + // Deletes a member from a board + // + // --- + // produces: + // - application/json + // parameters: + // - name: boardID + // in: path + // description: Board ID + // required: true + // type: string + // - name: userID + // in: path + // description: User ID + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + boardID := mux.Vars(r)["boardID"] + paramsUserID := mux.Vars(r)["userID"] + userID := getUserID(r) + + if paramsUserID != userID && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) + return + } + + board, err := a.app.GetBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err) + return + } + + auditRec := a.makeAuditRecord(r, "deleteMember", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardID", boardID) + auditRec.AddMeta("addedUserID", paramsUserID) + + deleteErr := a.app.DeleteBoardMember(boardID, paramsUserID) + if errors.Is(deleteErr, app.ErrBoardMemberIsLastAdmin) { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", deleteErr) + return + } + if deleteErr != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", deleteErr) + return + } + + a.logger.Debug("DeleteMember", + mlog.String("boardID", boardID), + mlog.String("addedUserID", paramsUserID), + ) + + // response + jsonStringResponse(w, http.StatusOK, "{}") + + auditRec.Success() +} + +func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /api/v1/boards-and-blocks insertBoardsAndBlocks + // + // Creates new boards and blocks + // + // --- + // produces: + // - application/json + // parameters: + // - name: Body + // in: body + // description: the boards and blocks to create + // required: true + // schema: + // "$ref": "#/definitions/BoardsAndBlocks" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // $ref: '#/definitions/BoardsAndBlocks' + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + userID := getUserID(r) + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var newBab *model.BoardsAndBlocks + if err = json.Unmarshal(requestBody, &newBab); err != nil { + // a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + for _, block := range newBab.Blocks { + // Error checking + if len(block.Type) < 1 { + message := fmt.Sprintf("missing type for block id %s", block.ID) + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) + return + } + + if block.CreateAt < 1 { + message := fmt.Sprintf("invalid createAt for block id %s", block.ID) + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) + return + } + + if block.UpdateAt < 1 { + message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) + return + } + } + + // permission check + createsPublicBoards := false + createsPrivateBoards := false + teamID := "" + for _, board := range newBab.Boards { + if board.Type == model.BoardTypeOpen { + createsPublicBoards = true + } + if board.Type == model.BoardTypePrivate { + createsPrivateBoards = true + } + + if teamID == "" { + teamID = board.TeamID + continue + } + + if teamID != board.TeamID { + message := "cannot create boards for multiple teams" + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) + return + } + + if board.ID == "" { + message := "boards need an ID to be referenced from the blocks" + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) + return + } + } + + // IDs of boards and blocks are used to confirm that they're + // linked and then regenerated by the server + newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) + return + } + + if createsPublicBoards && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePublicChannel) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create public boards"}) + return + } + + if createsPrivateBoards && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePrivateChannel) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create private boards"}) + return + } + + auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("teamID", teamID) + auditRec.AddMeta("userID", userID) + auditRec.AddMeta("boardsCount", len(newBab.Boards)) + auditRec.AddMeta("blocksCount", len(newBab.Blocks)) + + // create boards and blocks + bab, err := a.app.CreateBoardsAndBlocks(newBab, userID, true) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) + return + } + + a.logger.Debug("CreateBoardsAndBlocks", + mlog.String("teamID", teamID), + mlog.String("userID", userID), + mlog.Int("boardCount", len(bab.Boards)), + mlog.Int("blockCount", len(bab.Blocks)), + ) + + data, err := json.Marshal(bab) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { + // swagger:operation PATCH /api/v1/boards-and-blocks patchBoardsAndBlocks + // + // Patches a set of related boards and blocks + // + // --- + // produces: + // - application/json + // parameters: + // - name: Body + // in: body + // description: the patches for the boards and blocks + // required: true + // schema: + // "$ref": "#/definitions/PatchBoardsAndBlocks" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // $ref: '#/definitions/BoardsAndBlocks' + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + userID := getUserID(r) + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var pbab *model.PatchBoardsAndBlocks + if err = json.Unmarshal(requestBody, &pbab); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + if err = pbab.IsValid(); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + teamID := "" + boardIDMap := map[string]bool{} + for i, boardID := range pbab.BoardIDs { + boardIDMap[boardID] = true + patch := pbab.BoardPatches[i] + + if err = patch.IsValid(); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board properties"}) + return + } + + if patch.Type != nil { + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"}) + return + } + } + + board, err2 := a.app.GetBoard(boardID) + if err2 != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) + return + } + + if teamID == "" { + teamID = board.TeamID + } + if teamID != board.TeamID { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) + return + } + } + + for _, blockID := range pbab.BlockIDs { + block, err2 := a.app.GetBlockByID(blockID) + if err2 != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) + return + } + if block == nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) + return + } + + if _, ok := boardIDMap[block.BoardID]; !ok { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) + return + } + } + + auditRec := a.makeAuditRecord(r, "patchBoardsAndBlocks", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardsCount", len(pbab.BoardIDs)) + auditRec.AddMeta("blocksCount", len(pbab.BlockIDs)) + + bab, err := a.app.PatchBoardsAndBlocks(pbab, userID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("PATCH BoardsAndBlocks", + mlog.Int("boardsCount", len(pbab.BoardIDs)), + mlog.Int("blocksCount", len(pbab.BlockIDs)), + ) + + data, err := json.Marshal(bab) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + // response + jsonBytesResponse(w, http.StatusOK, data) + + auditRec.Success() +} + +func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { + // swagger:operation DELETE /api/v1/boards-and-blocks deleteBoardsAndBlocks + // + // Deletes boards and blocks + // + // --- + // produces: + // - application/json + // parameters: + // - name: Body + // in: body + // description: the boards and blocks to delete + // required: true + // schema: + // "$ref": "#/definitions/DeleteBoardsAndBlocks" + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + userID := getUserID(r) + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + var dbab *model.DeleteBoardsAndBlocks + if err = json.Unmarshal(requestBody, &dbab); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + // user must have permission to delete all the boards, and that + // would include the permission to manage their blocks + teamID := "" + for _, boardID := range dbab.Boards { + // all boards in the request should belong to the same team + board, err := a.app.GetBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + if teamID == "" { + teamID = board.TeamID + } + if teamID != board.TeamID { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) + return + } + + // permission check + if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { + a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"}) + return + } + } + + if err := dbab.IsValid(); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) + return + } + + auditRec := a.makeAuditRecord(r, "deleteBoardsAndBlocks", audit.Fail) + defer a.audit.LogRecord(audit.LevelModify, auditRec) + auditRec.AddMeta("boardsCount", len(dbab.Boards)) + auditRec.AddMeta("blocksCount", len(dbab.Blocks)) + + if err := a.app.DeleteBoardsAndBlocks(dbab, userID); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + + a.logger.Debug("DELETE BoardsAndBlocks", + mlog.Int("boardsCount", len(dbab.Boards)), + mlog.Int("blocksCount", len(dbab.Blocks)), + ) + + // response + jsonStringResponse(w, http.StatusOK, "{}") + auditRec.Success() +} + // Response helpers func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) { @@ -1805,6 +3764,7 @@ func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message mlog.String("msg", message), mlog.String("api", api), ) + w.Header().Set("Content-Type", "application/json") data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code}) if err != nil { @@ -1814,27 +3774,6 @@ func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message _, _ = w.Write(data) } -func (a *API) errorResponseWithCode(w http.ResponseWriter, api string, statusCode int, errorCode int, message string, sourceError error) { - a.logger.Error("API ERROR", - mlog.Int("status", statusCode), - mlog.Int("code", errorCode), - mlog.Err(sourceError), - mlog.String("msg", message), - mlog.String("api", api), - ) - w.Header().Set("Content-Type", "application/json") - data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: errorCode}) - if err != nil { - data = []byte("{}") - } - w.WriteHeader(statusCode) - _, _ = w.Write(data) -} - -func (a *API) noContainerErrorResponse(w http.ResponseWriter, api string, sourceError error) { - a.errorResponseWithCode(w, api, http.StatusBadRequest, ErrorNoWorkspaceCode, ErrorNoWorkspaceMessage, sourceError) -} - func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nolint:unparam w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) @@ -1846,21 +3785,3 @@ func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint: w.WriteHeader(code) _, _ = w.Write(json) } - -func (a *API) handleGetUserWorkspaces(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userWorkspaces, err := a.app.GetUserWorkspaces(session.UserID) - if err != nil { - a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) - return - } - - data, err := json.Marshal(userWorkspaces) - if err != nil { - a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} diff --git a/server/api/archive.go b/server/api/archive.go index 508c353f9..190c9c750 100644 --- a/server/api/archive.go +++ b/server/api/archive.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "github.com/gorilla/mux" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/audit" ) @@ -13,26 +14,20 @@ const ( archiveExtension = ".boardarchive" ) -func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v1/workspaces/{workspaceID}/archive/export archiveExport +func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/boards/{boardID}/archive/export archiveExportBoard // - // Exports an archive of all blocks for one or more boards. If board_id is provided then - // only that board will be exported, otherwise all boards in the workspace are exported. + // Exports an archive of all blocks for one boards. // // --- // produces: // - application/json // parameters: - // - name: workspaceID + // - name: boardID // in: path - // description: Workspace ID + // description: Id of board to export // required: true // type: string - // - name: board_id - // in: path - // description: Id of board to to export - // required: false - // type: string // security: // - BearerAuth: [] // responses: @@ -47,25 +42,92 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - query := r.URL.Query() - boardID := query.Get("board_id") - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) - return - } + vars := mux.Vars(r) + boardID := vars["boardID"] - auditRec := a.makeAuditRecord(r, "archiveExport", audit.Fail) + auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail) defer a.audit.LogRecord(audit.LevelRead, auditRec) auditRec.AddMeta("BoardID", boardID) - var boardIDs []string - if boardID != "" { - boardIDs = []string{boardID} + board, err := a.app.GetBoard(boardID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return } + if board == nil { + a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) + return + } + opts := model.ExportArchiveOptions{ - WorkspaceID: container.WorkspaceID, - BoardIDs: boardIDs, + TeamID: board.TeamID, + BoardIDs: []string{board.ID}, + } + + filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", "attachment; filename="+filename) + w.Header().Set("Content-Transfer-Encoding", "binary") + + if err := a.app.ExportArchive(w, opts); err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + } + + auditRec.Success() +} + +func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /api/v1/teams/{teamID}/archive/export archiveExportTeam + // + // Exports an archive of all blocks for all the boards in a team. + // + // --- + // produces: + // - application/json + // parameters: + // - name: teamID + // in: path + // description: Id of team + // required: true + // type: string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // content: + // application-octet-stream: + // type: string + // format: binary + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + vars := mux.Vars(r) + teamID := vars["teamID"] + + ctx := r.Context() + session, _ := ctx.Value(sessionContextKey).(*model.Session) + userID := session.UserID + + auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + auditRec.AddMeta("TeamID", teamID) + + boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID) + if err != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) + return + } + ids := []string{} + for _, board := range boards { + ids = append(ids, board.ID) + } + + opts := model.ExportArchiveOptions{ + TeamID: teamID, + BoardIDs: ids, } filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) @@ -81,7 +143,7 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) { } func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /api/v1/workspaces/{workspaceID}/archive/import archiveImport + // swagger:operation POST /api/v1/boards/{boardID}/archive/import archiveImport // // Import an archive of boards. // @@ -91,7 +153,7 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { // consumes: // - multipart/form-data // parameters: - // - name: workspaceID + // - name: boardID // in: path // description: Workspace ID // required: true @@ -111,16 +173,13 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" - container, err := a.getContainer(r) - if err != nil { - a.noContainerErrorResponse(w, r.URL.Path, err) - return - } - ctx := r.Context() session, _ := ctx.Value(sessionContextKey).(*model.Session) userID := session.UserID + vars := mux.Vars(r) + teamID := vars["teamID"] + file, handle, err := r.FormFile(UploadFormFileKey) if err != nil { fmt.Fprintf(w, "%v", err) @@ -134,8 +193,8 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("size", handle.Size) opt := model.ImportArchiveOptions{ - WorkspaceID: container.WorkspaceID, - ModifiedBy: userID, + TeamID: teamID, + ModifiedBy: userID, } if err := a.app.ImportArchive(file, opt); err != nil { diff --git a/server/api/audit.go b/server/api/audit.go index 4cfd79ce1..875f5c28c 100644 --- a/server/api/audit.go +++ b/server/api/audit.go @@ -17,12 +17,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin userID = session.UserID } - workspaceID := "unknown" - container, err := a.getContainer(r) - if err == nil { - workspaceID = container.WorkspaceID - } - + teamID := "unknown" rec := &audit.Record{ APIPath: r.URL.Path, Event: event, @@ -31,7 +26,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin SessionID: sessionID, Client: r.UserAgent(), IPAddress: r.RemoteAddr, - Meta: []audit.Meta{{K: audit.KeyWorkspaceID, V: workspaceID}}, + Meta: []audit.Meta{{K: audit.KeyTeamID, V: teamID}}, } return rec diff --git a/server/api/auth.go b/server/api/auth.go index e38d53c65..b5fa26789 100644 --- a/server/api/auth.go +++ b/server/api/auth.go @@ -302,13 +302,13 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { // Validate token if len(registerData.Token) > 0 { - workspace, err2 := a.app.GetRootWorkspace() + team, err2 := a.app.GetRootTeam() if err2 != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) return } - if registerData.Token != workspace.SignupToken { + if registerData.Token != team.SignupToken { a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid token", nil) return } diff --git a/server/app/app.go b/server/app/app.go index ed58085c8..641f060cd 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -1,12 +1,16 @@ package app import ( + "time" + "github.com/mattermost/focalboard/server/auth" "github.com/mattermost/focalboard/server/services/config" "github.com/mattermost/focalboard/server/services/metrics" "github.com/mattermost/focalboard/server/services/notify" + "github.com/mattermost/focalboard/server/services/permissions" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/webhook" + "github.com/mattermost/focalboard/server/utils" "github.com/mattermost/focalboard/server/ws" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -14,6 +18,12 @@ import ( "github.com/mattermost/mattermost-server/v6/shared/filestore" ) +const ( + blockChangeNotifierQueueSize = 100 + blockChangeNotifierPoolSize = 10 + blockChangeNotifierShutdownTimeout = time.Second * 10 +) + type Services struct { Auth *auth.Auth Store store.Store @@ -22,19 +32,21 @@ type Services struct { Metrics *metrics.Metrics Notifications *notify.Service Logger *mlog.Logger + Permissions permissions.PermissionsService SkipTemplateInit bool } type App struct { - config *config.Configuration - store store.Store - auth *auth.Auth - wsAdapter ws.Adapter - filesBackend filestore.FileBackend - webhook *webhook.Client - metrics *metrics.Metrics - notifications *notify.Service - logger *mlog.Logger + config *config.Configuration + store store.Store + auth *auth.Auth + wsAdapter ws.Adapter + filesBackend filestore.FileBackend + webhook *webhook.Client + metrics *metrics.Metrics + notifications *notify.Service + logger *mlog.Logger + blockChangeNotifier *utils.CallbackQueue } func (a *App) SetConfig(config *config.Configuration) { @@ -43,15 +55,16 @@ func (a *App) SetConfig(config *config.Configuration) { func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App { app := &App{ - config: config, - store: services.Store, - auth: services.Auth, - wsAdapter: wsAdapter, - filesBackend: services.FilesBackend, - webhook: services.Webhook, - metrics: services.Metrics, - notifications: services.Notifications, - logger: services.Logger, + config: config, + store: services.Store, + auth: services.Auth, + wsAdapter: wsAdapter, + filesBackend: services.FilesBackend, + webhook: services.Webhook, + metrics: services.Metrics, + notifications: services.Notifications, + logger: services.Logger, + blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger), } app.initialize(services.SkipTemplateInit) return app diff --git a/server/app/auth.go b/server/app/auth.go index 376b8980e..46cf7474a 100644 --- a/server/app/auth.go +++ b/server/app/auth.go @@ -3,7 +3,6 @@ package app import ( "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/auth" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -25,8 +24,8 @@ func (a *App) GetSession(token string) (*model.Session, error) { } // IsValidReadToken validates the read token for a block. -func (a *App) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) { - return a.auth.IsValidReadToken(c, blockID, readToken) +func (a *App) IsValidReadToken(boardID string, readToken string) (bool, error) { + return a.auth.IsValidReadToken(boardID, readToken) } // GetRegisteredUserCount returns the number of registered users. diff --git a/server/app/blocks.go b/server/app/blocks.go index b1193113b..3cd480764 100644 --- a/server/app/blocks.go +++ b/server/app/blocks.go @@ -1,137 +1,192 @@ package app import ( + "errors" "fmt" "path/filepath" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/notify" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) -func (a *App) GetBlocks(c store.Container, parentID string, blockType string) ([]model.Block, error) { +var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards") + +func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) { + if boardID == "" { + return []model.Block{}, nil + } + if blockType != "" && parentID != "" { - return a.store.GetBlocksWithParentAndType(c, parentID, blockType) + return a.store.GetBlocksWithParentAndType(boardID, parentID, blockType) } if blockType != "" { - return a.store.GetBlocksWithType(c, blockType) + return a.store.GetBlocksWithType(boardID, blockType) } - return a.store.GetBlocksWithParent(c, parentID) + return a.store.GetBlocksWithParent(boardID, parentID) } -func (a *App) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) { - return a.store.GetBlocksWithRootID(c, rootID) +func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) { + board, err := a.GetBoard(boardID) + if err != nil { + return nil, err + } + if board == nil { + return nil, fmt.Errorf("cannot fetch board %s for DuplicateBlock: %w", boardID, err) + } + + blocks, err := a.store.DuplicateBlock(boardID, blockID, userID, asTemplate) + if err != nil { + return nil, err + } + + a.blockChangeNotifier.Enqueue(func() error { + for _, block := range blocks { + a.wsAdapter.BroadcastBlockChange(board.TeamID, block) + } + return nil + }) + return blocks, err } -func (a *App) GetRootID(c store.Container, blockID string) (string, error) { - return a.store.GetRootID(c, blockID) +func (a *App) GetBlocksWithBoardID(boardID string) ([]model.Block, error) { + return a.store.GetBlocksWithBoardID(boardID) } -func (a *App) GetParentID(c store.Container, blockID string) (string, error) { - return a.store.GetParentID(c, blockID) -} - -func (a *App) PatchBlock(c store.Container, blockID string, blockPatch *model.BlockPatch, modifiedByID string) error { - oldBlock, err := a.store.GetBlock(c, blockID) +func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error { + oldBlock, err := a.store.GetBlock(blockID) if err != nil { return nil } - err = a.store.PatchBlock(c, blockID, blockPatch, modifiedByID) + board, err := a.store.GetBoard(oldBlock.BoardID) + if err != nil { + return err + } + + err = a.store.PatchBlock(blockID, blockPatch, modifiedByID) if err != nil { return err } a.metrics.IncrementBlocksPatched(1) - block, err := a.store.GetBlock(c, blockID) + block, err := a.store.GetBlock(blockID) if err != nil { return nil } - a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block) - go func() { + a.blockChangeNotifier.Enqueue(func() error { + // broadcast on websocket + a.wsAdapter.BroadcastBlockChange(board.TeamID, *block) + + // broadcast on webhooks a.webhook.NotifyUpdate(*block) - a.notifyBlockChanged(notify.Update, c, block, oldBlock, modifiedByID) - }() + + // send notifications + a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID) + return nil + }) return nil } -func (a *App) PatchBlocks(c store.Container, 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)) for _, blockID := range blockPatches.BlockIDs { - oldBlock, err := a.store.GetBlock(c, blockID) + oldBlock, err := a.store.GetBlock(blockID) if err != nil { return nil } oldBlocks = append(oldBlocks, *oldBlock) } - err := a.store.PatchBlocks(c, blockPatches, modifiedByID) + err := a.store.PatchBlocks(blockPatches, modifiedByID) if err != nil { return err } - a.metrics.IncrementBlocksPatched(len(oldBlocks)) - for i, blockID := range blockPatches.BlockIDs { - newBlock, err := a.store.GetBlock(c, blockID) - if err != nil { - return nil - } - a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *newBlock) - go func(currentIndex int) { + a.blockChangeNotifier.Enqueue(func() error { + a.metrics.IncrementBlocksPatched(len(oldBlocks)) + for i, blockID := range blockPatches.BlockIDs { + newBlock, err := a.store.GetBlock(blockID) + if err != nil { + return nil + } + a.wsAdapter.BroadcastBlockChange(teamID, *newBlock) a.webhook.NotifyUpdate(*newBlock) - a.notifyBlockChanged(notify.Update, c, newBlock, &oldBlocks[currentIndex], modifiedByID) - }(i) - } - + a.notifyBlockChanged(notify.Update, newBlock, &oldBlocks[i], modifiedByID) + } + return nil + }) return nil } -func (a *App) InsertBlock(c store.Container, block model.Block, modifiedByID string) error { - err := a.store.InsertBlock(c, &block, modifiedByID) +func (a *App) InsertBlock(block model.Block, modifiedByID string) error { + board, bErr := a.store.GetBoard(block.BoardID) + if bErr != nil { + return bErr + } + + err := a.store.InsertBlock(&block, modifiedByID) if err == nil { - a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, block) - a.metrics.IncrementBlocksInserted(1) - go func() { + a.blockChangeNotifier.Enqueue(func() error { + a.wsAdapter.BroadcastBlockChange(board.TeamID, block) + a.metrics.IncrementBlocksInserted(1) a.webhook.NotifyUpdate(block) - a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID) - }() + a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID) + return nil + }) } return err } -func (a *App) InsertBlocks(c store.Container, blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) { +func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) { + if len(blocks) == 0 { + return []model.Block{}, nil + } + + // all blocks must belong to the same board + boardID := blocks[0].BoardID + for _, block := range blocks { + if block.BoardID != boardID { + return nil, ErrBlocksFromMultipleBoards + } + } + + board, err := a.store.GetBoard(boardID) + if err != nil { + return nil, err + } + needsNotify := make([]model.Block, 0, len(blocks)) for i := range blocks { - err := a.store.InsertBlock(c, &blocks[i], modifiedByID) + err := a.store.InsertBlock(&blocks[i], modifiedByID) if err != nil { return nil, err } - blocks[i].WorkspaceID = c.WorkspaceID needsNotify = append(needsNotify, blocks[i]) - a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, blocks[i]) + a.wsAdapter.BroadcastBlockChange(board.TeamID, blocks[i]) a.metrics.IncrementBlocksInserted(1) } - go func() { + a.blockChangeNotifier.Enqueue(func() error { for _, b := range needsNotify { block := b a.webhook.NotifyUpdate(block) if allowNotifications { - a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID) + a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID) } } - }() + return nil + }) return blocks, nil } -func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks []model.Block) error { +func (a *App) CopyCardFiles(sourceBoardID string, blocks []model.Block) error { // Images attached in cards have a path comprising the card's board ID. // When we create a template from this board, we need to copy the files // with the new board ID in path. @@ -139,7 +194,7 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks // template) to fail to load. // look up ID of source board, which may be different than the blocks. - board, err := a.GetBlockByID(store.Container{}, sourceBoardID) + board, err := a.GetBlockByID(sourceBoardID) if err != nil || board == nil { return fmt.Errorf("cannot fetch board %s for CopyCardFiles: %w", sourceBoardID, err) } @@ -153,8 +208,8 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks ext := filepath.Ext(fileName.(string)) destFilename := utils.NewID(utils.IDTypeNone) + ext - sourceFilePath := filepath.Join(board.WorkspaceID, sourceBoardID, fileName.(string)) - destinationFilePath := filepath.Join(destWorkspaceID, block.RootID, destFilename) + sourceFilePath := filepath.Join(sourceBoardID, fileName.(string)) + destinationFilePath := filepath.Join(block.BoardID, destFilename) a.logger.Debug( "Copying card file", @@ -179,24 +234,26 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks return nil } -func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model.Block, error) { +func (a *App) GetSubTree(boardID, blockID string, levels int, opts model.QuerySubtreeOptions) ([]model.Block, error) { // Only 2 or 3 levels are supported for now if levels >= 3 { - return a.store.GetSubTree3(c, blockID, model.QuerySubtreeOptions{}) + return a.store.GetSubTree3(boardID, blockID, opts) } - return a.store.GetSubTree2(c, blockID, model.QuerySubtreeOptions{}) + + return a.store.GetSubTree2(boardID, blockID, opts) } -func (a *App) GetAllBlocks(c store.Container) ([]model.Block, error) { - return a.store.GetAllBlocks(c) +func (a *App) GetBlockByID(blockID string) (*model.Block, error) { + return a.store.GetBlock(blockID) } -func (a *App) GetBlockByID(c store.Container, blockID string) (*model.Block, error) { - return a.store.GetBlock(c, blockID) -} +func (a *App) DeleteBlock(blockID string, modifiedBy string) error { + block, err := a.store.GetBlock(blockID) + if err != nil { + return err + } -func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) error { - block, err := a.store.GetBlock(c, blockID) + board, err := a.store.GetBoard(block.BoardID) if err != nil { return err } @@ -206,7 +263,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) return nil } - err = a.store.DeleteBlock(c, blockID, modifiedBy) + err = a.store.DeleteBlock(blockID, modifiedBy) if err != nil { return err } @@ -214,7 +271,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) if block.Type == model.TypeImage { fileName, fileIDExists := block.Fields["fileId"] if fileName, fileIDIsString := fileName.(string); fileIDExists && fileIDIsString { - filePath := filepath.Join(block.WorkspaceID, block.RootID, fileName) + filePath := filepath.Join(block.BoardID, fileName) err = a.filesBackend.RemoveFile(filePath) if err != nil { @@ -225,31 +282,32 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) } } - a.wsAdapter.BroadcastBlockDelete(c.WorkspaceID, blockID, block.ParentID) - a.metrics.IncrementBlocksDeleted(1) - go func() { - a.notifyBlockChanged(notify.Delete, c, block, block, modifiedBy) - }() + a.blockChangeNotifier.Enqueue(func() error { + a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID) + a.metrics.IncrementBlocksDeleted(1) + a.notifyBlockChanged(notify.Delete, block, block, modifiedBy) + return nil + }) return nil } -func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string) error { - blocks, err := a.store.GetBlockHistory(c, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true}) +func (a *App) UndeleteBlock(blockID string, modifiedBy string) error { + blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true}) if err != nil { return err } if len(blocks) == 0 { - // deleting non-existing block not considered an error + // undeleting non-existing block not considered an error return nil } - err = a.store.UndeleteBlock(c, blockID, modifiedBy) + err = a.store.UndeleteBlock(blockID, modifiedBy) if err != nil { return err } - block, err := a.store.GetBlock(c, blockID) + block, err := a.store.GetBlock(blockID) if err != nil { return err } @@ -259,12 +317,19 @@ func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string return nil } - a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block) - a.metrics.IncrementBlocksInserted(1) - go func() { + board, err := a.store.GetBoard(block.BoardID) + if err != nil { + return err + } + + a.blockChangeNotifier.Enqueue(func() error { + a.wsAdapter.BroadcastBlockChange(board.TeamID, *block) + a.metrics.IncrementBlocksInserted(1) a.webhook.NotifyUpdate(*block) - a.notifyBlockChanged(notify.Add, c, block, nil, modifiedBy) - }() + a.notifyBlockChanged(notify.Add, block, nil, modifiedBy) + return nil + }) + return nil } @@ -272,13 +337,17 @@ func (a *App) GetBlockCountsByType() (map[string]int64, error) { return a.store.GetBlockCountsByType() } -func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block *model.Block, oldBlock *model.Block, modifiedByID string) { +func (a *App) GetBlocksForBoard(boardID string) ([]model.Block, error) { + return a.store.GetBlocksForBoard(boardID) +} + +func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) { if a.notifications == nil { return } // find card and board for the changed block. - board, card, err := a.store.GetBoardAndCard(c, block) + board, card, err := a.getBoardAndCard(block) if err != nil { a.logger.Error("Error notifying for block change; cannot determine board or card", mlog.Err(err)) return @@ -286,7 +355,7 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block evt := notify.BlockChangeEvent{ Action: action, - Workspace: c.WorkspaceID, + TeamID: board.TeamID, Board: board, Card: card, BlockChanged: block, @@ -295,3 +364,35 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block } a.notifications.BlockChanged(evt) } + +const ( + maxSearchDepth = 50 +) + +// getBoardAndCard returns the first parent of type `card` its board for the specified block. +// `board` and/or `card` may return nil without error if the block does not belong to a board or card. +func (a *App) getBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) { + board, err = a.store.GetBoard(block.BoardID) + if err != nil { + return board, card, err + } + + var count int // don't let invalid blocks hierarchy cause infinite loop. + iter := block + for { + count++ + if card == nil && iter.Type == model.TypeCard { + card = iter + } + + if iter.ParentID == "" || (board != nil && card != nil) || count > maxSearchDepth { + break + } + + iter, err = a.store.GetBlock(iter.ParentID) + if err != nil || iter == nil { + return board, card, err + } + } + return board, card, nil +} diff --git a/server/app/blocks_test.go b/server/app/blocks_test.go index 78df69780..b1f0f582c 100644 --- a/server/app/blocks_test.go +++ b/server/app/blocks_test.go @@ -3,10 +3,9 @@ package app import ( "testing" + "github.com/golang/mock/gomock" "github.com/mattermost/focalboard/server/model" - "github.com/golang/mock/gomock" - st "github.com/mattermost/focalboard/server/services/store" "github.com/stretchr/testify/require" ) @@ -18,47 +17,28 @@ func (be blockError) Error() string { return be.msg } -func TestGetParentID(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - container := st.Container{ - WorkspaceID: "0", - } - t.Run("success query", func(t *testing.T) { - th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("test-parent-id", nil) - result, err := th.App.GetParentID(container, "test-id") - require.NoError(t, err) - require.Equal(t, "test-parent-id", result) - }) - - t.Run("fail query", func(t *testing.T) { - th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("", blockError{"block-not-found"}) - _, err := th.App.GetParentID(container, "test-id") - require.Error(t, err) - require.ErrorIs(t, err, blockError{"block-not-found"}) - }) -} - func TestInsertBlock(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - container := st.Container{ - WorkspaceID: "0", - } - t.Run("success scenerio", func(t *testing.T) { - block := model.Block{} - th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(nil) - err := th.App.InsertBlock(container, block, "user-id-1") + boardID := testBoardID + block := model.Block{BoardID: boardID} + board := &model.Board{ID: boardID} + th.Store.EXPECT().GetBoard(boardID).Return(board, nil) + th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(nil) + th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) + err := th.App.InsertBlock(block, "user-id-1") require.NoError(t, err) }) t.Run("error scenerio", func(t *testing.T) { - block := model.Block{} - th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(blockError{"error"}) - err := th.App.InsertBlock(container, block, "user-id-1") + boardID := testBoardID + block := model.Block{BoardID: boardID} + board := &model.Board{ID: boardID} + th.Store.EXPECT().GetBoard(boardID).Return(board, nil) + th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(blockError{"error"}) + err := th.App.InsertBlock(block, "user-id-1") require.Error(t, err, "error") }) } @@ -67,20 +47,17 @@ func TestPatchBlocks(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - container := st.Container{ - WorkspaceID: "0", - } t.Run("patchBlocks success scenerio", func(t *testing.T) { blockPatches := model.BlockPatchBatch{} - th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil) - err := th.App.PatchBlocks(container, &blockPatches, "user-id-1") + th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil) + err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") require.NoError(t, err) }) t.Run("patchBlocks error scenerio", func(t *testing.T) { blockPatches := model.BlockPatchBatch{} - th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"}) - err := th.App.PatchBlocks(container, &blockPatches, "user-id-1") + th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"}) + err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") require.Error(t, err, "error") }) } @@ -89,27 +66,32 @@ func TestDeleteBlock(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - container := st.Container{ - WorkspaceID: "0", - } - t.Run("success scenerio", func(t *testing.T) { + boardID := testBoardID + board := &model.Board{ID: boardID} block := model.Block{ - ID: "block-id", + ID: "block-id", + BoardID: board.ID, } - th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) - th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) - err := th.App.DeleteBlock(container, "block-id", "user-id-1") + th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) + th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) + th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil) + th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) + err := th.App.DeleteBlock("block-id", "user-id-1") require.NoError(t, err) }) t.Run("error scenerio", func(t *testing.T) { + boardID := testBoardID + board := &model.Board{ID: boardID} block := model.Block{ - ID: "block-id", + ID: "block-id", + BoardID: board.ID, } - th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) - th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) - err := th.App.DeleteBlock(container, "block-id", "user-id-1") + th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) + th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) + th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil) + err := th.App.DeleteBlock("block-id", "user-id-1") require.Error(t, err, "error") }) } @@ -118,22 +100,22 @@ func TestUndeleteBlock(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - container := st.Container{ - WorkspaceID: "0", - } - t.Run("success scenerio", func(t *testing.T) { + boardID := testBoardID + board := &model.Board{ID: boardID} block := model.Block{ - ID: "block-id", + ID: "block-id", + BoardID: board.ID, } th.Store.EXPECT().GetBlockHistory( - gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}), ).Return([]model.Block{block}, nil) - th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) - th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) - err := th.App.UndeleteBlock(container, "block-id", "user-id-1") + th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) + th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) + th.Store.EXPECT().GetBoard(boardID).Return(board, nil) + th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) + err := th.App.UndeleteBlock("block-id", "user-id-1") require.NoError(t, err) }) @@ -142,13 +124,12 @@ func TestUndeleteBlock(t *testing.T) { ID: "block-id", } th.Store.EXPECT().GetBlockHistory( - gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}), ).Return([]model.Block{block}, nil) - th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) - th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) - err := th.App.UndeleteBlock(container, "block-id", "user-id-1") + th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) + th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) + err := th.App.UndeleteBlock("block-id", "user-id-1") require.Error(t, err, "error") }) } diff --git a/server/app/boards.go b/server/app/boards.go new file mode 100644 index 000000000..6fc0b76bb --- /dev/null +++ b/server/app/boards.go @@ -0,0 +1,254 @@ +package app + +import ( + "database/sql" + "errors" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/utils" +) + +var ( + ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins") + ErrNewBoardCannotHaveID = errors.New("new board cannot have an ID") +) + +func (a *App) GetBoard(boardID string) (*model.Board, error) { + board, err := a.store.GetBoard(boardID) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + return board, nil +} + +func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { + bab, members, err := a.store.DuplicateBoard(boardID, userID, toTeam, asTemplate) + if err != nil { + return nil, nil, err + } + go func() { + teamID := "" + for _, board := range bab.Boards { + teamID = board.TeamID + a.wsAdapter.BroadcastBoardChange(teamID, board) + } + for _, block := range bab.Blocks { + a.wsAdapter.BroadcastBlockChange(teamID, block) + } + for _, member := range members { + a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member) + } + }() + return bab, members, err +} + +func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) { + return a.store.GetBoardsForUserAndTeam(userID, teamID) +} + +func (a *App) GetTemplateBoards(teamID string) ([]*model.Board, error) { + return a.store.GetTemplateBoards(teamID) +} + +func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) { + if board.ID != "" { + return nil, ErrNewBoardCannotHaveID + } + board.ID = utils.NewID(utils.IDTypeBoard) + + var newBoard *model.Board + var member *model.BoardMember + var err error + if addMember { + newBoard, member, err = a.store.InsertBoardWithAdmin(board, userID) + } else { + newBoard, err = a.store.InsertBoard(board, userID) + } + + if err != nil { + return nil, err + } + + go func() { + a.wsAdapter.BroadcastBoardChange(newBoard.TeamID, newBoard) + + if addMember { + a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, newBoard.ID, member) + } + }() + + return newBoard, nil +} + +func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) { + updatedBoard, err := a.store.PatchBoard(boardID, patch, userID) + if err != nil { + return nil, err + } + + go func() { + a.wsAdapter.BroadcastBoardChange(updatedBoard.TeamID, updatedBoard) + }() + + return updatedBoard, nil +} + +func (a *App) DeleteBoard(boardID, userID string) error { + board, err := a.store.GetBoard(boardID) + if errors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return err + } + + if err := a.store.DeleteBoard(boardID, userID); err != nil { + return err + } + + go func() { + a.wsAdapter.BroadcastBoardDelete(board.TeamID, boardID) + }() + + return nil +} + +func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { + return a.store.GetMembersForBoard(boardID) +} + +func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) { + return a.store.GetMembersForUser(userID) +} + +func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) { + board, err := a.store.GetBoard(member.BoardID) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + + existingMembership, err := a.store.GetMemberForBoard(member.BoardID, member.UserID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, err + } + + if existingMembership != nil { + return existingMembership, nil + } + + newMember, err := a.store.SaveMember(member) + if err != nil { + return nil, err + } + + go func() { + a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member) + }() + + return newMember, nil +} + +func (a *App) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, error) { + board, bErr := a.store.GetBoard(member.BoardID) + if errors.Is(bErr, sql.ErrNoRows) { + return nil, nil + } + if bErr != nil { + return nil, bErr + } + + oldMember, err := a.store.GetMemberForBoard(member.BoardID, member.UserID) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + + // if we're updating an admin, we need to check that there is at + // least still another admin on the board + if oldMember.SchemeAdmin && !member.SchemeAdmin { + isLastAdmin, err2 := a.isLastAdmin(member.UserID, member.BoardID) + if err2 != nil { + return nil, err2 + } + if isLastAdmin { + return nil, ErrBoardMemberIsLastAdmin + } + } + + newMember, err := a.store.SaveMember(member) + if err != nil { + return nil, err + } + + go func() { + a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member) + }() + + return newMember, nil +} + +func (a *App) isLastAdmin(userID, boardID string) (bool, error) { + members, err := a.store.GetMembersForBoard(boardID) + if err != nil { + return false, err + } + + for _, m := range members { + if m.SchemeAdmin && m.UserID != userID { + return false, nil + } + } + return true, nil +} + +func (a *App) DeleteBoardMember(boardID, userID string) error { + board, bErr := a.store.GetBoard(boardID) + if errors.Is(bErr, sql.ErrNoRows) { + return nil + } + if bErr != nil { + return bErr + } + + oldMember, err := a.store.GetMemberForBoard(boardID, userID) + if errors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return err + } + + // if we're removing an admin, we need to check that there is at + // least still another admin on the board + if oldMember.SchemeAdmin { + isLastAdmin, err := a.isLastAdmin(userID, boardID) + if err != nil { + return err + } + if isLastAdmin { + return ErrBoardMemberIsLastAdmin + } + } + + if err := a.store.DeleteMember(boardID, userID); err != nil { + return err + } + + go func() { + a.wsAdapter.BroadcastMemberDelete(board.TeamID, boardID, userID) + }() + + return nil +} + +func (a *App) SearchBoardsForUserAndTeam(term, userID, teamID string) ([]*model.Board, error) { + return a.store.SearchBoardsForUserAndTeam(term, userID, teamID) +} diff --git a/server/app/boards_and_blocks.go b/server/app/boards_and_blocks.go new file mode 100644 index 000000000..1208d4676 --- /dev/null +++ b/server/app/boards_and_blocks.go @@ -0,0 +1,126 @@ +package app + +import ( + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/notify" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, addMember bool) (*model.BoardsAndBlocks, error) { + var newBab *model.BoardsAndBlocks + var members []*model.BoardMember + var err error + + if addMember { + newBab, members, err = a.store.CreateBoardsAndBlocksWithAdmin(bab, userID) + } else { + newBab, err = a.store.CreateBoardsAndBlocks(bab, userID) + } + + if err != nil { + return nil, err + } + + // all new boards should belong to the same team + teamID := newBab.Boards[0].TeamID + + // This can be synchronous because this action is not common + for _, board := range newBab.Boards { + a.wsAdapter.BroadcastBoardChange(teamID, board) + } + + for _, block := range newBab.Blocks { + b := block + a.wsAdapter.BroadcastBlockChange(teamID, b) + a.metrics.IncrementBlocksInserted(1) + a.webhook.NotifyUpdate(b) + a.notifyBlockChanged(notify.Add, &b, nil, userID) + } + + if addMember { + for _, member := range members { + a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member) + } + } + + return newBab, nil +} + +func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { + oldBlocksMap := map[string]*model.Block{} + for _, blockID := range pbab.BlockIDs { + block, err := a.store.GetBlock(blockID) + if err != nil { + return nil, err + } + oldBlocksMap[blockID] = block + } + + bab, err := a.store.PatchBoardsAndBlocks(pbab, userID) + if err != nil { + return nil, err + } + + a.blockChangeNotifier.Enqueue(func() error { + teamID := bab.Boards[0].TeamID + + for _, block := range bab.Blocks { + oldBlock, ok := oldBlocksMap[block.ID] + if !ok { + a.logger.Error("Error notifying for block change on patch boards and blocks; cannot get old block", mlog.String("blockID", block.ID)) + continue + } + + b := block + a.metrics.IncrementBlocksPatched(1) + a.wsAdapter.BroadcastBlockChange(teamID, b) + a.webhook.NotifyUpdate(b) + a.notifyBlockChanged(notify.Update, &b, oldBlock, userID) + } + + for _, board := range bab.Boards { + a.wsAdapter.BroadcastBoardChange(board.TeamID, board) + } + return nil + }) + + return bab, nil +} + +func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error { + firstBoard, err := a.store.GetBoard(dbab.Boards[0]) + if err != nil { + return err + } + + // we need the block entity to notify of the block changes, so we + // fetch and store the blocks first + blocks := []*model.Block{} + for _, blockID := range dbab.Blocks { + block, err := a.store.GetBlock(blockID) + if err != nil { + return err + } + blocks = append(blocks, block) + } + + if err := a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil { + return err + } + + a.blockChangeNotifier.Enqueue(func() error { + for _, block := range blocks { + a.wsAdapter.BroadcastBlockDelete(firstBoard.TeamID, block.ID, block.BoardID) + a.metrics.IncrementBlocksDeleted(1) + a.notifyBlockChanged(notify.Update, block, block, userID) + } + + for _, boardID := range dbab.Boards { + a.wsAdapter.BroadcastBoardDelete(firstBoard.TeamID, boardID) + } + return nil + }) + + return nil +} diff --git a/server/app/category.go b/server/app/category.go new file mode 100644 index 000000000..b80cf9f33 --- /dev/null +++ b/server/app/category.go @@ -0,0 +1,103 @@ +package app + +import ( + "errors" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/utils" +) + +var ( + ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user") + ErrorCategoryDeleted = errors.New("category is deleted") +) + +func (a *App) CreateCategory(category *model.Category) (*model.Category, error) { + category.Hydrate() + if err := category.IsValid(); err != nil { + return nil, err + } + + if err := a.store.CreateCategory(*category); err != nil { + return nil, err + } + + createdCategory, err := a.store.GetCategory(category.ID) + if err != nil { + return nil, err + } + + go func() { + a.wsAdapter.BroadcastCategoryChange(*createdCategory) + }() + + return createdCategory, nil +} + +func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) { + // verify if category belongs to the user + existingCategory, err := a.store.GetCategory(category.ID) + if err != nil { + return nil, err + } + + if existingCategory.DeleteAt != 0 { + return nil, ErrorCategoryDeleted + } + + if existingCategory.UserID != category.UserID { + return nil, ErrorCategoryPermissionDenied + } + + category.UpdateAt = utils.GetMillis() + if err = category.IsValid(); err != nil { + return nil, err + } + if err = a.store.UpdateCategory(*category); err != nil { + return nil, err + } + + updatedCategory, err := a.store.GetCategory(category.ID) + if err != nil { + return nil, err + } + + go func() { + a.wsAdapter.BroadcastCategoryChange(*updatedCategory) + }() + + return updatedCategory, nil +} + +func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category, error) { + existingCategory, err := a.store.GetCategory(categoryID) + if err != nil { + return nil, err + } + + // category is already deleted. This avoids + // overriding the original deleted at timestamp + if existingCategory.DeleteAt != 0 { + return existingCategory, nil + } + + // verify if category belongs to the user + if existingCategory.UserID != userID { + return nil, ErrorCategoryPermissionDenied + } + + if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil { + return nil, err + } + + deletedCategory, err := a.store.GetCategory(categoryID) + if err != nil { + return nil, err + } + + go func() { + a.wsAdapter.BroadcastCategoryChange(*deletedCategory) + }() + + return deletedCategory, nil +} diff --git a/server/app/category_blocks.go b/server/app/category_blocks.go new file mode 100644 index 000000000..158b448e0 --- /dev/null +++ b/server/app/category_blocks.go @@ -0,0 +1,26 @@ +package app + +import "github.com/mattermost/focalboard/server/model" + +func (a *App) GetUserCategoryBlocks(userID, teamID string) ([]model.CategoryBlocks, error) { + return a.store.GetUserCategoryBlocks(userID, teamID) +} + +func (a *App) AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID string) error { + err := a.store.AddUpdateCategoryBlock(userID, categoryID, blockID) + if err != nil { + return err + } + + go func() { + a.wsAdapter.BroadcastCategoryBlockChange( + teamID, + userID, + model.BlockCategoryWebsocketData{ + BlockID: blockID, + CategoryID: categoryID, + }) + }() + + return nil +} diff --git a/server/app/export.go b/server/app/export.go index aae56b1db..d1de4798a 100644 --- a/server/app/export.go +++ b/server/app/export.go @@ -7,7 +7,6 @@ import ( "io" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/wiggin77/merror" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -18,10 +17,7 @@ var ( ) func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) { - container := store.Container{ - WorkspaceID: opt.WorkspaceID, - } - boards, err := a.getBoardsForArchive(container, opt.BoardIDs) + boards, err := a.getBoardsForArchive(opt.BoardIDs) if err != nil { return err } @@ -71,7 +67,7 @@ func (a *App) writeArchiveVersion(zw *zip.Writer) error { } // writeArchiveBoard writes a single board to the archive in a zip directory. -func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.ExportArchiveOptions) error { +func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error { // create a directory per board w, err := zw.Create(board.ID + "/board.jsonl") if err != nil { @@ -79,18 +75,14 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.Exp } // write the board block first - if err = a.writeArchiveBlockLine(w, board); err != nil { + if err = a.writeArchiveBoardLine(w, board); err != nil { return err } var files []string - container := store.Container{ - WorkspaceID: opt.WorkspaceID, - } - // write the board's blocks // TODO: paginate this - blocks, err := a.GetBlocksWithRootID(container, board.ID) + blocks, err := a.GetBlocksWithBoardID(board.ID) if err != nil { return err } @@ -143,6 +135,32 @@ func (a *App) writeArchiveBlockLine(w io.Writer, block model.Block) error { return err } +// writeArchiveBlockLine writes a single block to the archive. +func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error { + b, err := json.Marshal(&board) + if err != nil { + return err + } + line := model.ArchiveLine{ + Type: "board", + Data: b, + } + + b, err = json.Marshal(&line) + if err != nil { + return err + } + + _, err = w.Write(b) + if err != nil { + return err + } + + // jsonl files need a newline + _, err = w.Write(newline) + return err +} + // writeArchiveFile writes a single file to the archive. func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error { dest, err := zw.Create(boardID + "/" + filename) @@ -150,12 +168,12 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, return err } - src, err := a.GetFileReader(opt.WorkspaceID, boardID, filename) + src, err := a.GetFileReader(opt.TeamID, boardID, filename) if err != nil { // just log this; image file is missing but we'll still export an equivalent board a.logger.Error("image file missing for export", mlog.String("filename", filename), - mlog.String("workspace_id", opt.WorkspaceID), + mlog.String("team_id", opt.TeamID), mlog.String("board_id", boardID), ) return nil @@ -168,27 +186,25 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, // getBoardsForArchive fetches all the specified boards, or all boards in the workspace/team // if `boardIDs` is empty. -func (a *App) getBoardsForArchive(container store.Container, boardIDs []string) ([]model.Block, error) { +func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) { if len(boardIDs) == 0 { - boards, err := a.GetBlocks(container, "", model.TypeBoard) - if err != nil { - return nil, fmt.Errorf("could not fetch all boards: %w", err) - } - return boards, nil + // TODO: implement this + // boards, err := a.GetAllBoards("", "board") + // if err != nil { + // return nil, fmt.Errorf("could not fetch all boards: %w", err) + // } + // return boards, nil + return []model.Board{}, nil } - boards := make([]model.Block, 0, len(boardIDs)) + boards := make([]model.Board, 0, len(boardIDs)) for _, id := range boardIDs { - b, err := a.GetBlockByID(container, id) + b, err := a.GetBoard(id) if err != nil { return nil, fmt.Errorf("could not fetch board %s: %w", id, err) } - if b.Type != model.TypeBoard { - return nil, fmt.Errorf("block %s is not a board: %w", b.ID, model.ErrInvalidBoardBlock) - } - boards = append(boards, *b) } return boards, nil diff --git a/server/app/files.go b/server/app/files.go index e1f9450a5..a2914cae4 100644 --- a/server/app/files.go +++ b/server/app/files.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost-server/v6/shared/filestore" ) -func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (string, error) { +func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) { // NOTE: File extension includes the dot fileExtension := strings.ToLower(filepath.Ext(filename)) if fileExtension == ".jpeg" { @@ -21,7 +21,7 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) ( } createdFilename := fmt.Sprintf(`%s%s`, utils.NewID(utils.IDTypeNone), fileExtension) - filePath := filepath.Join(workspaceID, rootID, createdFilename) + filePath := filepath.Join(teamID, rootID, createdFilename) _, appErr := a.filesBackend.WriteFile(reader, filePath) if appErr != nil { @@ -31,14 +31,14 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) ( return createdFilename, nil } -func (a *App) GetFileReader(workspaceID, rootID, filename string) (filestore.ReadCloseSeeker, error) { - filePath := filepath.Join(workspaceID, rootID, filename) +func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) { + filePath := filepath.Join(teamID, rootID, filename) exists, err := a.filesBackend.FileExists(filePath) if err != nil { return nil, err } // FIXUP: Check the deprecated old location - if workspaceID == "0" && !exists { + if teamID == "0" && !exists { oldExists, err2 := a.filesBackend.FileExists(filename) if err2 != nil { return nil, err2 diff --git a/server/app/files_test.go b/server/app/files_test.go index 099d573aa..44ea15058 100644 --- a/server/app/files_test.go +++ b/server/app/files_test.go @@ -5,16 +5,17 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" "github.com/mattermost/mattermost-server/v6/shared/filestore" "github.com/mattermost/mattermost-server/v6/shared/filestore/mocks" - "github.com/stretchr/testify/assert" ) const ( testFileName = "temp-file-name" - testRootID = "test-root-id" - testFilePath = "1/test-root-id/temp-file-name" + testBoardID = "test-board-id" + testFilePath = "1/test-board-id/temp-file-name" ) type TestError struct{} @@ -45,7 +46,7 @@ func TestGetFileReader(t *testing.T) { mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) - actual, _ := th.App.GetFileReader("1", testRootID, testFileName) + actual, _ := th.App.GetFileReader("1", testBoardID, testFileName) assert.Equal(t, mockedReadCloseSeek, actual) }) @@ -71,7 +72,7 @@ func TestGetFileReader(t *testing.T) { mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) - actual, err := th.App.GetFileReader("1", testRootID, testFileName) + actual, err := th.App.GetFileReader("1", testBoardID, testFileName) assert.Error(t, err, mockedError) assert.Nil(t, actual) }) @@ -98,13 +99,13 @@ func TestGetFileReader(t *testing.T) { mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) - actual, err := th.App.GetFileReader("1", testRootID, testFileName) + actual, err := th.App.GetFileReader("1", testBoardID, testFileName) assert.Error(t, err, mockedError) assert.Nil(t, actual) }) t.Run("should move file from old filepath to new filepath, if file doesnot exists in new filepath and workspace id is 0", func(t *testing.T) { - filePath := "0/test-root-id/temp-file-name" + filePath := "0/test-board-id/temp-file-name" workspaceid := "0" mockedFileBackend := &mocks.FileBackend{} th.App.filesBackend = mockedFileBackend @@ -134,12 +135,12 @@ func TestGetFileReader(t *testing.T) { mockedFileBackend.On("MoveFile", testFileName, filePath).Return(moveFileFunc) mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc) - actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName) + actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName) assert.Equal(t, mockedReadCloseSeek, actual) }) t.Run("should return file reader, if file doesnot exists in new filepath and old file path", func(t *testing.T) { - filePath := "0/test-root-id/temp-file-name" + filePath := "0/test-board-id/temp-file-name" fileName := testFileName workspaceid := "0" mockedFileBackend := &mocks.FileBackend{} @@ -170,7 +171,7 @@ func TestGetFileReader(t *testing.T) { mockedFileBackend.On("MoveFile", fileName, filePath).Return(moveFileFunc) mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc) - actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName) + actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName) assert.Equal(t, mockedReadCloseSeek, actual) }) } @@ -186,7 +187,7 @@ func TestSaveFile(t *testing.T) { writeFileFunc := func(reader io.Reader, path string) int64 { paths := strings.Split(path, "/") assert.Equal(t, "1", paths[0]) - assert.Equal(t, testRootID, paths[1]) + assert.Equal(t, testBoardID, paths[1]) fileName = paths[2] return int64(10) } @@ -196,7 +197,7 @@ func TestSaveFile(t *testing.T) { } mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testRootID, fileName) + actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName) assert.Equal(t, fileName, actual) assert.Nil(t, err) }) @@ -209,7 +210,7 @@ func TestSaveFile(t *testing.T) { writeFileFunc := func(reader io.Reader, path string) int64 { paths := strings.Split(path, "/") assert.Equal(t, "1", paths[0]) - assert.Equal(t, "test-root-id", paths[1]) + assert.Equal(t, "test-board-id", paths[1]) assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) return int64(10) } @@ -219,7 +220,7 @@ func TestSaveFile(t *testing.T) { } mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName) + actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName) assert.Nil(t, err) assert.NotNil(t, actual) }) @@ -233,7 +234,7 @@ func TestSaveFile(t *testing.T) { writeFileFunc := func(reader io.Reader, path string) int64 { paths := strings.Split(path, "/") assert.Equal(t, "1", paths[0]) - assert.Equal(t, "test-root-id", paths[1]) + assert.Equal(t, "test-board-id", paths[1]) assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) return int64(10) } @@ -243,7 +244,7 @@ func TestSaveFile(t *testing.T) { } mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName) + actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName) assert.Equal(t, "", actual) assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error()) }) diff --git a/server/app/helper_test.go b/server/app/helper_test.go index 1c21c2c71..452007f6e 100644 --- a/server/app/helper_test.go +++ b/server/app/helper_test.go @@ -29,11 +29,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { defer ctrl.Finish() cfg := config.Configuration{} store := mockstore.NewMockStore(ctrl) - - auth := auth.New(&cfg, store) + auth := auth.New(&cfg, store, nil) logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug) sessionToken := "TESTTOKEN" - wsserver := ws.NewServer(auth, sessionToken, false, logger) + wsserver := ws.NewServer(auth, sessionToken, false, logger, store) webhook := webhook.NewClient(&cfg, logger) metricsService := metrics.NewMetrics(metrics.InstanceInfo{}) @@ -49,6 +48,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { app2 := New(&cfg, wsserver, appServices) tearDown := func() { + app2.Shutdown() if logger != nil { _ = logger.Shutdown() } diff --git a/server/app/import.go b/server/app/import.go index 5cb431a38..878b8ef89 100644 --- a/server/app/import.go +++ b/server/app/import.go @@ -14,7 +14,6 @@ import ( "github.com/krolaw/zipstream" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -84,7 +83,7 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { continue } // save file with original filename so it matches name in image block. - filePath := filepath.Join(opt.WorkspaceID, boardID, filename) + filePath := filepath.Join(opt.TeamID, boardID, filename) _, err := a.filesBackend.WriteFile(zr, filePath) if err != nil { return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err) @@ -103,7 +102,10 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (string, error) { // TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks. // We don't want to load the whole file in memory, even though it's a single board. - blocks := make([]model.Block, 0, 10) + boardsAndBlocks := &model.BoardsAndBlocks{ + Blocks: make([]model.Block, 0, 10), + Boards: make([]*model.Board, 0, 10), + } lineReader := bufio.NewReader(r) userID := opt.ModifiedBy @@ -137,7 +139,16 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str } block.ModifiedBy = userID block.UpdateAt = now - blocks = append(blocks, block) + boardsAndBlocks.Blocks = append(boardsAndBlocks.Blocks, block) + case "board": + var board model.Board + if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil { + return "", fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2) + } + board.ModifiedBy = userID + board.UpdateAt = now + board.TeamID = opt.TeamID + boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, &board) default: return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type) } @@ -154,36 +165,33 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str } modInfoCache := make(map[string]interface{}) - modBlocks := make([]model.Block, 0, len(blocks)) - for _, block := range blocks { - b := block - if opt.BlockModifier != nil && !opt.BlockModifier(&b, modInfoCache) { + modBoards := make([]*model.Board, 0, len(boardsAndBlocks.Boards)) + for _, board := range boardsAndBlocks.Boards { + b := *board + if opt.BoardModifier != nil && !opt.BoardModifier(&b, modInfoCache) { a.logger.Debug("skipping insert block per block modifier", - mlog.String("blockID", block.ID), - mlog.String("block_type", block.Type.String()), + mlog.String("blockID", board.ID), ) continue } - modBlocks = append(modBlocks, b) - } - - blocks = model.GenerateBlockIDs(modBlocks, a.logger) - - container := store.Container{ - WorkspaceID: opt.WorkspaceID, + modBoards = append(modBoards, &b) } + boardsAndBlocks.Boards = modBoards var err error - blocks, err = a.InsertBlocks(container, blocks, opt.ModifiedBy, false) + boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger) + if err != nil { + return "", fmt.Errorf("error inserting archive blocks: %w", err) + } + + boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false) if err != nil { return "", fmt.Errorf("error inserting archive blocks: %w", err) } // find new board id - for _, block := range blocks { - if block.Type == model.TypeBoard { - return block.ID, nil - } + for _, board := range boardsAndBlocks.Boards { + return board.ID, nil } return "", fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock) } diff --git a/server/app/initialize.go b/server/app/initialize.go index 200bb6430..edca2ec8f 100644 --- a/server/app/initialize.go +++ b/server/app/initialize.go @@ -1,6 +1,10 @@ package app -import "github.com/mattermost/mattermost-server/v6/shared/mlog" +import ( + "context" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) // initialize is called when the App is first created. func (a *App) initialize(skipTemplateInit bool) { @@ -10,3 +14,13 @@ func (a *App) initialize(skipTemplateInit bool) { } } } + +func (a *App) Shutdown() { + if a.blockChangeNotifier != nil { + ctx, cancel := context.WithTimeout(context.Background(), blockChangeNotifierShutdownTimeout) + defer cancel() + if !a.blockChangeNotifier.Shutdown(ctx) { + a.logger.Warn("blockChangeNotifier shutdown timed out") + } + } +} diff --git a/server/app/onboarding.go b/server/app/onboarding.go index 9a976cec4..369e32227 100644 --- a/server/app/onboarding.go +++ b/server/app/onboarding.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" ) const ( @@ -21,17 +20,12 @@ const ( var ( errUnableToFindWelcomeBoard = errors.New("unable to find welcome board in newly created blocks") + errCannotCreateBoard = errors.New("new board wasn't created") ) -func (a *App) PrepareOnboardingTour(userID string) (string, string, error) { - // create a private workspace for the user - workspaceID, err := a.store.CreatePrivateWorkspace(userID) - if err != nil { - return "", "", err - } - +func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, string, error) { // copy the welcome board into this workspace - boardID, err := a.createWelcomeBoard(userID, workspaceID) + boardID, err := a.createWelcomeBoard(userID, teamID) if err != nil { return "", "", err } @@ -48,18 +42,18 @@ func (a *App) PrepareOnboardingTour(userID string) (string, string, error) { return "", "", err } - return workspaceID, boardID, nil + return teamID, boardID, nil } func (a *App) getOnboardingBoardID() (string, error) { - blocks, err := a.store.GetDefaultTemplateBlocks() + boards, err := a.store.GetTemplateBoards(globalTeamID) if err != nil { return "", err } var onboardingBoardID string - for _, block := range blocks { - if block.Type == model.TypeBoard && block.Title == WelcomeBoardTitle { + for _, block := range boards { + if block.Title == WelcomeBoardTitle { onboardingBoardID = block.ID break } @@ -72,42 +66,20 @@ func (a *App) getOnboardingBoardID() (string, error) { return onboardingBoardID, nil } -func (a *App) createWelcomeBoard(userID, workspaceID string) (string, error) { +func (a *App) createWelcomeBoard(userID, teamID string) (string, error) { onboardingBoardID, err := a.getOnboardingBoardID() if err != nil { return "", err } - blocks, err := a.GetSubTree(store.Container{WorkspaceID: "0"}, onboardingBoardID, 3) + bab, _, err := a.DuplicateBoard(onboardingBoardID, userID, teamID, false) if err != nil { return "", err } - blocks = model.GenerateBlockIDs(blocks, a.logger) - - if errUpdateFileIDs := a.CopyCardFiles(onboardingBoardID, workspaceID, blocks); errUpdateFileIDs != nil { - return "", errUpdateFileIDs + if len(bab.Boards) != 1 { + return "", errCannotCreateBoard } - // we're copying from a global template, so we need to set the - // `isTemplate` flag to false on the board - var welcomeBoardID string - for i := range blocks { - if blocks[i].Type == model.TypeBoard { - blocks[i].Fields["isTemplate"] = false - - if blocks[i].Title == WelcomeBoardTitle { - welcomeBoardID = blocks[i].ID - break - } - } - } - - model.StampModificationMetadata(userID, blocks, nil) - _, err = a.InsertBlocks(store.Container{WorkspaceID: workspaceID}, blocks, userID, false) - if err != nil { - return "", err - } - - return welcomeBoardID, nil + return bab.Boards[0].ID, nil } diff --git a/server/app/onboarding_test.go b/server/app/onboarding_test.go index 78c1a862c..1388add44 100644 --- a/server/app/onboarding_test.go +++ b/server/app/onboarding_test.go @@ -3,44 +3,32 @@ package app import ( "testing" - "github.com/golang/mock/gomock" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/stretchr/testify/assert" ) +const ( + testTeamID = "team_id" +) + func TestPrepareOnboardingTour(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() t.Run("base case", func(t *testing.T) { - welcomeBoard := model.Block{ - ID: "block_id_1", - Type: model.TypeBoard, - Title: "Welcome to Boards!", - Fields: map[string]interface{}{ - "isTemplate": true, - }, + teamID := testTeamID + userID := "user_id_1" + welcomeBoard := model.Board{ + ID: "board_id_1", + Title: "Welcome to Boards!", + TeamID: "0", + IsTemplate: true, } - blocks := []model.Block{welcomeBoard} - th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) - - th.Store.EXPECT().GetSubTree3( - store.Container{WorkspaceID: "0"}, - "block_id_1", - gomock.Any(), - ).Return([]model.Block{welcomeBoard}, nil) - - th.Store.EXPECT().InsertBlock( - store.Container{WorkspaceID: "workspace_id_1"}, - gomock.Any(), - "user_id_1", - ).Return(nil) - - th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil) - - th.Store.EXPECT().CreatePrivateWorkspace("user_id_1").Return("workspace_id_1", nil) + th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) + th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, + nil, nil) + th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil) userPropPatch := model.UserPropPatch{ UpdatedFields: map[string]string{ @@ -50,11 +38,11 @@ func TestPrepareOnboardingTour(t *testing.T) { }, } - th.Store.EXPECT().PatchUserProps("user_id_1", userPropPatch).Return(nil) + th.Store.EXPECT().PatchUserProps(userID, userPropPatch).Return(nil) - workspaceID, boardID, err := th.App.PrepareOnboardingTour("user_id_1") + teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID) assert.NoError(t, err) - assert.Equal(t, "workspace_id_1", workspaceID) + assert.Equal(t, testTeamID, teamID) assert.NotEmpty(t, boardID) }) } @@ -64,88 +52,41 @@ func TestCreateWelcomeBoard(t *testing.T) { defer tearDown() t.Run("base case", func(t *testing.T) { - welcomeBoard := model.Block{ - ID: "block_id_1", - Type: model.TypeBoard, - Title: "Welcome to Boards!", - Fields: map[string]interface{}{ - "isTemplate": true, - }, + teamID := testTeamID + userID := "user_id_1" + welcomeBoard := model.Board{ + ID: "board_id_1", + Title: "Welcome to Boards!", + TeamID: "0", + IsTemplate: true, } + th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) + th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false). + Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil) + th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil) - blocks := []model.Block{welcomeBoard} - th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) - - th.Store.EXPECT().GetSubTree3( - store.Container{WorkspaceID: "0"}, - "block_id_1", - gomock.Any(), - ).Return([]model.Block{welcomeBoard}, nil) - - th.Store.EXPECT().InsertBlock( - store.Container{WorkspaceID: "workspace_id_1"}, - gomock.Any(), - "user_id_1", - ).Return(nil) - - th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil) - - boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") + boardID, err := th.App.createWelcomeBoard(userID, teamID) assert.Nil(t, err) assert.NotEmpty(t, boardID) }) t.Run("template doesn't contain a board", func(t *testing.T) { - welcomeBoard := model.Block{ - ID: "block_id_1", - Type: model.TypeComment, - Title: "Welcome to Boards!", - } - blocks := []model.Block{welcomeBoard} - th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) - - th.Store.EXPECT().GetSubTree3( - store.Container{WorkspaceID: "0"}, - "buixxjic3xjfkieees4iafdrznc", - gomock.Any(), - ).Return([]model.Block{welcomeBoard}, nil) - - th.Store.EXPECT().InsertBlock( - store.Container{WorkspaceID: "workspace_id_1"}, - gomock.Any(), - "user_id_1", - ).Return(nil) - - boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") + teamID := testTeamID + th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil) + boardID, err := th.App.createWelcomeBoard("user_id_1", teamID) assert.Error(t, err) assert.Empty(t, boardID) }) t.Run("template doesn't contain the welcome board", func(t *testing.T) { - welcomeBoard := model.Block{ - ID: "block_id_1", - Type: model.TypeBoard, - Title: "Jean luc Picard", - Fields: map[string]interface{}{ - "isTemplate": true, - }, + teamID := testTeamID + welcomeBoard := model.Board{ + ID: "board_id_1", + Title: "Other template", + TeamID: teamID, + IsTemplate: true, } - - blocks := []model.Block{welcomeBoard} - th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) - - th.Store.EXPECT().GetSubTree3( - store.Container{WorkspaceID: "0"}, - "buixxjic3xjfkieees4iafdrznc", - gomock.Any(), - ).Return([]model.Block{welcomeBoard}, nil) - - th.Store.EXPECT().InsertBlock( - store.Container{WorkspaceID: "workspace_id_1"}, - gomock.Any(), - "user_id_1", - ).Return(nil) - + th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") assert.Error(t, err) assert.Empty(t, boardID) @@ -157,24 +98,13 @@ func TestGetOnboardingBoardID(t *testing.T) { defer tearDown() t.Run("base case", func(t *testing.T) { - board := model.Block{ - ID: "board_id_1", - Type: model.TypeBoard, - Title: "Welcome to Boards!", + welcomeBoard := model.Board{ + ID: "board_id_1", + Title: "Welcome to Boards!", + TeamID: "0", + IsTemplate: true, } - - card := model.Block{ - ID: "card_id_1", - Type: model.TypeCard, - ParentID: board.ID, - } - - blocks := []model.Block{ - board, - card, - } - - th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) + th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) onboardingBoardID, err := th.App.getOnboardingBoardID() assert.NoError(t, err) @@ -182,9 +112,7 @@ func TestGetOnboardingBoardID(t *testing.T) { }) t.Run("no blocks found", func(t *testing.T) { - blocks := []model.Block{} - - th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) + th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil) onboardingBoardID, err := th.App.getOnboardingBoardID() assert.Error(t, err) @@ -192,24 +120,13 @@ func TestGetOnboardingBoardID(t *testing.T) { }) t.Run("onboarding board doesn't exists", func(t *testing.T) { - board := model.Block{ - ID: "board_id_1", - Type: model.TypeBoard, - Title: "Some board title", + welcomeBoard := model.Board{ + ID: "board_id_1", + Title: "Other template", + TeamID: "0", + IsTemplate: true, } - - card := model.Block{ - ID: "card_id_1", - Type: model.TypeCard, - ParentID: board.ID, - } - - blocks := []model.Block{ - board, - card, - } - - th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) + th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) onboardingBoardID, err := th.App.getOnboardingBoardID() assert.Error(t, err) diff --git a/server/app/sharing.go b/server/app/sharing.go index 781449f1f..86aa7e4ef 100644 --- a/server/app/sharing.go +++ b/server/app/sharing.go @@ -5,11 +5,10 @@ import ( "errors" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" ) -func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, error) { - sharing, err := a.store.GetSharing(c, rootID) +func (a *App) GetSharing(boardID string) (*model.Sharing, error) { + sharing, err := a.store.GetSharing(boardID) if errors.Is(err, sql.ErrNoRows) { return nil, nil } @@ -19,6 +18,6 @@ func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, erro return sharing, nil } -func (a *App) UpsertSharing(c store.Container, sharing model.Sharing) error { - return a.store.UpsertSharing(c, sharing) +func (a *App) UpsertSharing(sharing model.Sharing) error { + return a.store.UpsertSharing(sharing) } diff --git a/server/app/sharing_test.go b/server/app/sharing_test.go index 551ada53e..ef63d6c15 100644 --- a/server/app/sharing_test.go +++ b/server/app/sharing_test.go @@ -4,9 +4,7 @@ import ( "database/sql" "testing" - "github.com/golang/mock/gomock" "github.com/mattermost/focalboard/server/model" - st "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -16,10 +14,6 @@ func TestGetSharing(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - container := st.Container{ - WorkspaceID: utils.NewID(utils.IDTypeWorkspace), - } - t.Run("should get a sharing successfully", func(t *testing.T) { want := &model.Sharing{ ID: utils.NewID(utils.IDTypeBlock), @@ -28,9 +22,9 @@ func TestGetSharing(t *testing.T) { ModifiedBy: "otherid", UpdateAt: utils.GetMillis(), } - th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(want, nil) + th.Store.EXPECT().GetSharing("test-id").Return(want, nil) - result, err := th.App.GetSharing(container, "test-id") + result, err := th.App.GetSharing("test-id") require.NoError(t, err) require.Equal(t, result, want) @@ -38,11 +32,11 @@ func TestGetSharing(t *testing.T) { }) t.Run("should fail to get a sharing", func(t *testing.T) { - th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return( + th.Store.EXPECT().GetSharing("test-id").Return( nil, errors.New("sharing not found"), ) - result, err := th.App.GetSharing(container, "test-id") + result, err := th.App.GetSharing("test-id") require.Nil(t, result) require.Error(t, err) @@ -50,11 +44,11 @@ func TestGetSharing(t *testing.T) { }) t.Run("should return a tuple of nil", func(t *testing.T) { - th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return( + th.Store.EXPECT().GetSharing("test-id").Return( nil, sql.ErrNoRows, ) - result, err := th.App.GetSharing(container, "test-id") + result, err := th.App.GetSharing("test-id") require.Nil(t, result) require.NoError(t, err) @@ -65,9 +59,6 @@ func TestUpsertSharing(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - container := st.Container{ - WorkspaceID: utils.NewID(utils.IDTypeWorkspace), - } sharing := model.Sharing{ ID: utils.NewID(utils.IDTypeBlock), Enabled: true, @@ -77,15 +68,15 @@ func TestUpsertSharing(t *testing.T) { } t.Run("should success to upsert sharing", func(t *testing.T) { - th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(nil) - err := th.App.UpsertSharing(container, sharing) + th.Store.EXPECT().UpsertSharing(sharing).Return(nil) + err := th.App.UpsertSharing(sharing) require.NoError(t, err) }) t.Run("should fail to upsert a sharing", func(t *testing.T) { - th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(errors.New("sharing not found")) - err := th.App.UpsertSharing(container, sharing) + th.Store.EXPECT().UpsertSharing(sharing).Return(errors.New("sharing not found")) + err := th.App.UpsertSharing(sharing) require.Error(t, err) require.Equal(t, "sharing not found", err.Error()) diff --git a/server/app/subscriptions.go b/server/app/subscriptions.go index 12cf939d1..aaf8ec316 100644 --- a/server/app/subscriptions.go +++ b/server/app/subscriptions.go @@ -2,41 +2,40 @@ package app import ( "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" ) -func (a *App) CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error) { - sub, err := a.store.CreateSubscription(c, sub) +func (a *App) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) { + sub, err := a.store.CreateSubscription(sub) if err != nil { return nil, err } - a.notifySubscriptionChanged(c, sub) + a.notifySubscriptionChanged(sub) return sub, nil } -func (a *App) DeleteSubscription(c store.Container, blockID string, subscriberID string) (*model.Subscription, error) { - sub, err := a.store.GetSubscription(c, blockID, subscriberID) +func (a *App) DeleteSubscription(blockID string, subscriberID string) (*model.Subscription, error) { + sub, err := a.store.GetSubscription(blockID, subscriberID) if err != nil { return nil, err } - if err := a.store.DeleteSubscription(c, blockID, subscriberID); err != nil { + if err := a.store.DeleteSubscription(blockID, subscriberID); err != nil { return nil, err } sub.DeleteAt = utils.GetMillis() - a.notifySubscriptionChanged(c, sub) + a.notifySubscriptionChanged(sub) return sub, nil } -func (a *App) GetSubscriptions(c store.Container, subscriberID string) ([]*model.Subscription, error) { - return a.store.GetSubscriptions(c, subscriberID) +func (a *App) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) { + return a.store.GetSubscriptions(subscriberID) } -func (a *App) notifySubscriptionChanged(c store.Container, subscription *model.Subscription) { +func (a *App) notifySubscriptionChanged(subscription *model.Subscription) { if a.notifications == nil { return } - a.notifications.BroadcastSubscriptionChange(c.WorkspaceID, subscription) + a.notifications.BroadcastSubscriptionChange(subscription) } diff --git a/server/app/teams.go b/server/app/teams.go new file mode 100644 index 000000000..097f61ed2 --- /dev/null +++ b/server/app/teams.go @@ -0,0 +1,68 @@ +package app + +import ( + "database/sql" + "errors" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/utils" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +func (a *App) GetRootTeam() (*model.Team, error) { + teamID := "0" + team, _ := a.store.GetTeam(teamID) + if team == nil { + team = &model.Team{ + ID: teamID, + SignupToken: utils.NewID(utils.IDTypeToken), + } + err := a.store.UpsertTeamSignupToken(*team) + if err != nil { + a.logger.Error("Unable to initialize team", mlog.Err(err)) + return nil, err + } + + team, err = a.store.GetTeam(teamID) + if err != nil { + a.logger.Error("Unable to get initialized team", mlog.Err(err)) + return nil, err + } + + a.logger.Info("initialized team") + } + + return team, nil +} + +func (a *App) GetTeam(id string) (*model.Team, error) { + team, err := a.store.GetTeam(id) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + return team, nil +} + +func (a *App) GetTeamsForUser(userID string) ([]*model.Team, error) { + return a.store.GetTeamsForUser(userID) +} + +func (a *App) DoesUserHaveTeamAccess(userID string, teamID string) bool { + return a.auth.DoesUserHaveTeamAccess(userID, teamID) +} + +func (a *App) UpsertTeamSettings(team model.Team) error { + return a.store.UpsertTeamSettings(team) +} + +func (a *App) UpsertTeamSignupToken(team model.Team) error { + return a.store.UpsertTeamSignupToken(team) +} + +func (a *App) GetTeamCount() (int64, error) { + return a.store.GetTeamCount() +} diff --git a/server/app/teams_test.go b/server/app/teams_test.go new file mode 100644 index 000000000..424d6d116 --- /dev/null +++ b/server/app/teams_test.go @@ -0,0 +1,152 @@ +package app + +import ( + "database/sql" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/mattermost/focalboard/server/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var errInvalidTeam = errors.New("invalid team id") + +var mockTeam = &model.Team{ + ID: "mock-team-id", + Title: "MockTeam", +} + +var errUpsertSignupToken = errors.New("upsert error") + +func TestGetRootTeam(t *testing.T) { + var newRootTeam = &model.Team{ + ID: "0", + Title: "NewRootTeam", + } + + testCases := []struct { + title string + teamToReturnBeforeUpsert *model.Team + teamToReturnAfterUpsert *model.Team + isError bool + }{ + { + "Success, Return new root team, when root team returned by mockstore is nil", + nil, + newRootTeam, + false, + }, + { + "Success, Return existing root team, when root team returned by mockstore is notnil", + newRootTeam, + nil, + false, + }, + { + "Fail, Return nil, when root team returned by mockstore is nil, and upsert new root team fails", + nil, + nil, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + th, tearDown := SetupTestHelper(t) + defer tearDown() + th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnBeforeUpsert, nil) + th.Store.EXPECT().UpsertTeamSignupToken(gomock.Any()).DoAndReturn( + func(arg0 model.Team) error { + if tc.isError { + return errUpsertSignupToken + } + th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnAfterUpsert, nil) + return nil + }) + rootTeam, err := th.App.GetRootTeam() + + if tc.isError { + require.Error(t, err) + } else { + assert.NotNil(t, rootTeam.ID) + assert.NotNil(t, rootTeam.SignupToken) + assert.Equal(t, "", rootTeam.ModifiedBy) + assert.Equal(t, int64(0), rootTeam.UpdateAt) + assert.Equal(t, "NewRootTeam", rootTeam.Title) + require.NoError(t, err) + require.NotNil(t, rootTeam) + } + }) + } +} + +func TestGetTeam(t *testing.T) { + th, tearDown := SetupTestHelper(t) + defer tearDown() + + testCases := []struct { + title string + teamID string + isError bool + }{ + { + "Success, Return new root team, when team returned by mockstore is not nil", + "mock-team-id", + false, + }, + { + "Success, Return nil, when get team returns an sql error", + "team-not-available-id", + false, + }, + { + "Fail, Return nil, when get team by mockstore returns an error", + "invalid-team-id", + true, + }, + } + + th.Store.EXPECT().GetTeam("mock-team-id").Return(mockTeam, nil) + th.Store.EXPECT().GetTeam("invalid-team-id").Return(nil, errInvalidTeam) + th.Store.EXPECT().GetTeam("team-not-available-id").Return(nil, sql.ErrNoRows) + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + t.Log(tc.title) + team, err := th.App.GetTeam(tc.teamID) + + if tc.isError { + require.Error(t, err) + } else if tc.teamID != "team-not-available-id" { + assert.NotNil(t, team.ID) + assert.NotNil(t, team.SignupToken) + assert.Equal(t, "mock-team-id", team.ID) + assert.Equal(t, "", team.ModifiedBy) + assert.Equal(t, int64(0), team.UpdateAt) + assert.Equal(t, "MockTeam", team.Title) + require.NoError(t, err) + require.NotNil(t, team) + } + }) + } +} + +func TestTeamOperations(t *testing.T) { + th, tearDown := SetupTestHelper(t) + defer tearDown() + + th.Store.EXPECT().UpsertTeamSettings(*mockTeam).Return(nil) + th.Store.EXPECT().UpsertTeamSignupToken(*mockTeam).Return(nil) + th.Store.EXPECT().GetTeamCount().Return(int64(10), nil) + + errUpsertTeamSettings := th.App.UpsertTeamSettings(*mockTeam) + assert.NoError(t, errUpsertTeamSettings) + + errUpsertTeamSignupToken := th.App.UpsertTeamSignupToken(*mockTeam) + assert.NoError(t, errUpsertTeamSignupToken) + + count, errGetTeamCount := th.App.GetTeamCount() + assert.NoError(t, errGetTeamCount) + assert.Equal(t, int64(10), count) +} diff --git a/server/app/templates.boardarchive b/server/app/templates.boardarchive index 2787cb16b..bb2808b42 100644 Binary files a/server/app/templates.boardarchive and b/server/app/templates.boardarchive differ diff --git a/server/app/templates.go b/server/app/templates.go index 8c014f8f1..da28e7567 100644 --- a/server/app/templates.go +++ b/server/app/templates.go @@ -14,21 +14,26 @@ import ( const ( defaultTemplateVersion = 2 + globalTeamID = "0" ) //go:embed templates.boardarchive var defTemplates []byte -// initializeTemplates imports default templates if the blocks table is empty. +func (a *App) InitTemplates() error { + return a.initializeTemplates() +} + +// initializeTemplates imports default templates if the boards table is empty. func (a *App) initializeTemplates() error { - blocks, err := a.store.GetDefaultTemplateBlocks() + boards, err := a.store.GetTemplateBoards(globalTeamID) if err != nil { return fmt.Errorf("cannot initialize templates: %w", err) } - a.logger.Debug("Fetched template blocks", mlog.Int("count", len(blocks))) + a.logger.Debug("Fetched template boards", mlog.Int("count", len(boards))) - isNeeded, reason := a.isInitializationNeeded(blocks) + isNeeded, reason := a.isInitializationNeeded(boards) if !isNeeded { a.logger.Debug("Template import not needed, skipping") return nil @@ -36,67 +41,55 @@ func (a *App) initializeTemplates() error { a.logger.Debug("Importing new default templates", mlog.String("reason", reason)) - if err := a.store.RemoveDefaultTemplates(blocks); err != nil { - return fmt.Errorf("cannot remove old templates: %w", err) + // Remove in case of newer Templates + if err = a.store.RemoveDefaultTemplates(boards); err != nil { + return fmt.Errorf("cannot remove old template boards: %w", err) } r := bytes.NewReader(defTemplates) opt := model.ImportArchiveOptions{ - WorkspaceID: "0", + TeamID: globalTeamID, ModifiedBy: "system", - BlockModifier: fixTemplateBlock, + BoardModifier: fixTemplateBoard, } - - return a.ImportArchive(r, opt) + if err = a.ImportArchive(r, opt); err != nil { + return fmt.Errorf("cannot initialize global templates for team %s: %w", globalTeamID, err) + } + return nil } // isInitializationNeeded returns true if the blocks table contains no default templates, // or contains at least one default template with an old version number. -func (a *App) isInitializationNeeded(blocks []model.Block) (bool, string) { - if len(blocks) == 0 { +func (a *App) isInitializationNeeded(boards []*model.Board) (bool, string) { + if len(boards) == 0 { return true, "no default templates found" } - // look for any template blocks with the wrong version number (or no version #). - for _, block := range blocks { - v, ok := block.Fields["templateVer"] - if !ok { - return true, "block missing templateVer" + // look for any built-in template boards with the wrong version number (or no version #). + for _, board := range boards { + // if not built-in board...skip + if board.CreatedBy != "system" { + continue } - version, ok := v.(float64) - if !ok { - return true, "templateVer NaN" - } - if version < defaultTemplateVersion { - return true, "templateVer too old" + if board.TemplateVersion < defaultTemplateVersion { + return true, "template_version too old" } } return false, "" } -// fixTemplateBlock fixes a block to be inserted as part of a template. -func fixTemplateBlock(block *model.Block, cache map[string]interface{}) bool { - // cache contains ids of skipped blocks. Ensure their children are skipped as well. - if _, ok := cache[block.ParentID]; ok { - cache[block.ID] = struct{}{} - return false - } - +// fixTemplateBoard fixes a board to be inserted as part of a template. +func fixTemplateBoard(board *model.Board, cache map[string]interface{}) bool { // filter out template blocks; we only want the non-template // blocks which we will turn into default template blocks. - if b, ok := block.Fields["isTemplate"]; ok { - if val, ok := b.(bool); ok && val { - cache[block.ID] = struct{}{} - return false - } + if board.IsTemplate { + cache[board.ID] = struct{}{} } // remove '(NEW)' from title & force template flag - if block.Type == model.TypeBoard { - block.Title = strings.ReplaceAll(block.Title, "(NEW)", "") - block.Fields["isTemplate"] = true - block.Fields["templateVer"] = defaultTemplateVersion - } + board.Title = strings.ReplaceAll(board.Title, "(NEW)", "") + board.IsTemplate = true + board.TemplateVersion = defaultTemplateVersion return true } diff --git a/server/app/user.go b/server/app/user.go index 87985c0b4..763479959 100644 --- a/server/app/user.go +++ b/server/app/user.go @@ -2,8 +2,12 @@ package app import "github.com/mattermost/focalboard/server/model" -func (a *App) GetWorkspaceUsers(workspaceID string) ([]*model.User, error) { - return a.store.GetUsersByWorkspace(workspaceID) +func (a *App) GetTeamUsers(teamID string) ([]*model.User, error) { + return a.store.GetUsersByTeam(teamID) +} + +func (a *App) SearchTeamUsers(teamID string, searchQuery string) ([]*model.User, error) { + return a.store.SearchUsersByTeam(teamID, searchQuery) } func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) { diff --git a/server/app/workspaces.go b/server/app/workspaces.go deleted file mode 100644 index 7b890f111..000000000 --- a/server/app/workspaces.go +++ /dev/null @@ -1,67 +0,0 @@ -package app - -import ( - "database/sql" - "errors" - - "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/utils" - - "github.com/mattermost/mattermost-server/v6/shared/mlog" -) - -func (a *App) GetRootWorkspace() (*model.Workspace, error) { - workspaceID := "0" - workspace, _ := a.store.GetWorkspace(workspaceID) - if workspace == nil { - workspace = &model.Workspace{ - ID: workspaceID, - SignupToken: utils.NewID(utils.IDTypeToken), - } - err := a.store.UpsertWorkspaceSignupToken(*workspace) - if err != nil { - a.logger.Error("Unable to initialize workspace", mlog.Err(err)) - return nil, err - } - workspace, err = a.store.GetWorkspace(workspaceID) - if err != nil { - a.logger.Error("Unable to get initialized workspace", mlog.Err(err)) - return nil, err - } - - a.logger.Info("initialized workspace") - } - - return workspace, nil -} - -func (a *App) GetWorkspace(id string) (*model.Workspace, error) { - workspace, err := a.store.GetWorkspace(id) - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - if err != nil { - return nil, err - } - return workspace, nil -} - -func (a *App) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool { - return a.auth.DoesUserHaveWorkspaceAccess(userID, workspaceID) -} - -func (a *App) UpsertWorkspaceSettings(workspace model.Workspace) error { - return a.store.UpsertWorkspaceSettings(workspace) -} - -func (a *App) UpsertWorkspaceSignupToken(workspace model.Workspace) error { - return a.store.UpsertWorkspaceSignupToken(workspace) -} - -func (a *App) GetWorkspaceCount() (int64, error) { - return a.store.GetWorkspaceCount() -} - -func (a *App) GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) { - return a.store.GetUserWorkspaces(userID) -} diff --git a/server/app/workspaces_test.go b/server/app/workspaces_test.go deleted file mode 100644 index c89eff837..000000000 --- a/server/app/workspaces_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package app - -import ( - "database/sql" - "errors" - "testing" - - "github.com/golang/mock/gomock" - "github.com/mattermost/focalboard/server/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var errInvalidWorkspace = errors.New("invalid workspace id") - -var mockWorkspace = &model.Workspace{ - ID: "mock-workspace-id", - Title: "MockWorkspace", -} - -var mockUserWorkspaces = []model.UserWorkspace{ - { - ID: "mock-user-workspace-id", - Title: "MockUserWorkspace", - }, -} - -var errUpsertSignupToken = errors.New("upsert error") - -func TestGetRootWorkspace(t *testing.T) { - var newRootWorkspace = &model.Workspace{ - ID: "0", - Title: "NewRootWorkspace", - } - - testCases := []struct { - title string - workSpaceToReturnBeforeUpsert *model.Workspace - workSpaceToReturnAfterUpsert *model.Workspace - isError bool - }{ - { - "Success, Return new root workspace, when root workspace returned by mockstore is nil", - nil, - newRootWorkspace, - false, - }, - { - "Success, Return existing root workspace, when root workspace returned by mockstore is notnil", - newRootWorkspace, - nil, - false, - }, - { - "Fail, Return nil, when root workspace returned by mockstore is nil, and upsert new root workspace fails", - nil, - nil, - true, - }, - } - - for _, eachTestacase := range testCases { - t.Run(eachTestacase.title, func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - t.Log(eachTestacase.title) - th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnBeforeUpsert, nil) - th.Store.EXPECT().UpsertWorkspaceSignupToken(gomock.Any()).DoAndReturn( - func(arg0 model.Workspace) error { - if eachTestacase.isError { - return errUpsertSignupToken - } - th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnAfterUpsert, nil) - return nil - }) - rootWorkSpace, err := th.App.GetRootWorkspace() - - if eachTestacase.isError { - require.Error(t, err) - } else { - assert.NotNil(t, rootWorkSpace.ID) - assert.NotNil(t, rootWorkSpace.SignupToken) - assert.Equal(t, "", rootWorkSpace.ModifiedBy) - assert.Equal(t, int64(0), rootWorkSpace.UpdateAt) - assert.Equal(t, "NewRootWorkspace", rootWorkSpace.Title) - require.NoError(t, err) - require.NotNil(t, rootWorkSpace) - } - }) - } -} - -func TestGetWorkspace(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - testCases := []struct { - title string - workspaceID string - isError bool - }{ - { - "Success, Return new root workspace, when workspace returned by mockstore is not nil", - "mock-workspace-id", - false, - }, - { - "Success, Return nil, when get workspace returns an sql error", - "workspace-not-available-id", - false, - }, - { - "Fail, Return nil, when get workspace by mockstore retruns an error", - "invalid-workspace-id", - true, - }, - } - - th.Store.EXPECT().GetWorkspace("mock-workspace-id").Return(mockWorkspace, nil) - th.Store.EXPECT().GetWorkspace("invalid-workspace-id").Return(nil, errInvalidWorkspace) - th.Store.EXPECT().GetWorkspace("workspace-not-available-id").Return(nil, sql.ErrNoRows) - for _, eachTestacase := range testCases { - t.Run(eachTestacase.title, func(t *testing.T) { - t.Log(eachTestacase.title) - workSpace, err := th.App.GetWorkspace(eachTestacase.workspaceID) - - if eachTestacase.isError { - require.Error(t, err) - } else if eachTestacase.workspaceID != "workspace-not-available-id" { - assert.NotNil(t, workSpace.ID) - assert.NotNil(t, workSpace.SignupToken) - assert.Equal(t, "mock-workspace-id", workSpace.ID) - assert.Equal(t, "", workSpace.ModifiedBy) - assert.Equal(t, int64(0), workSpace.UpdateAt) - assert.Equal(t, "MockWorkspace", workSpace.Title) - require.NoError(t, err) - require.NotNil(t, workSpace) - } - }) - } -} - -func TestWorkspaceOperations(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.Store.EXPECT().UpsertWorkspaceSettings(*mockWorkspace).Return(nil) - th.Store.EXPECT().UpsertWorkspaceSignupToken(*mockWorkspace).Return(nil) - th.Store.EXPECT().GetWorkspaceCount().Return(int64(10), nil) - th.Store.EXPECT().GetUserWorkspaces("mock-user-id").Return(mockUserWorkspaces, nil) - - errUpsertWorkspaceSettings := th.App.UpsertWorkspaceSettings(*mockWorkspace) - assert.NoError(t, errUpsertWorkspaceSettings) - - errUpsertWorkspaceSignupToken := th.App.UpsertWorkspaceSignupToken(*mockWorkspace) - assert.NoError(t, errUpsertWorkspaceSignupToken) - - count, errGetWorkspaceCount := th.App.GetWorkspaceCount() - assert.NoError(t, errGetWorkspaceCount) - assert.Equal(t, int64(10), count) - - userWorkSpace, errGetUserWorkSpace := th.App.GetUserWorkspaces("mock-user-id") - assert.NoError(t, errGetUserWorkSpace) - assert.NotNil(t, userWorkSpace) -} diff --git a/server/auth/auth.go b/server/auth/auth.go index 7af6c36f4..486484f19 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -6,6 +6,7 @@ import ( "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/permissions" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" "github.com/pkg/errors" @@ -13,19 +14,20 @@ import ( type AuthInterface interface { GetSession(token string) (*model.Session, error) - IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) - DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool + IsValidReadToken(boardID string, readToken string) (bool, error) + DoesUserHaveTeamAccess(userID string, teamID string) bool } // Auth authenticates sessions. type Auth struct { - config *config.Configuration - store store.Store + config *config.Configuration + store store.Store + permissions permissions.PermissionsService } // New returns a new Auth. -func New(config *config.Configuration, store store.Store) *Auth { - return &Auth{config: config, store: store} +func New(config *config.Configuration, store store.Store, permissions permissions.PermissionsService) *Auth { + return &Auth{config: config, store: store, permissions: permissions} } // GetSession Get a user active session and refresh the session if needed. @@ -44,14 +46,9 @@ func (a *Auth) GetSession(token string) (*model.Session, error) { return session, nil } -// IsValidReadToken validates the read token for a block. -func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) { - rootID, err := a.store.GetRootID(c, blockID) - if err != nil { - return false, err - } - - sharing, err := a.store.GetSharing(c, rootID) +// IsValidReadToken validates the read token for a board. +func (a *Auth) IsValidReadToken(boardID string, readToken string) (bool, error) { + sharing, err := a.store.GetSharing(boardID) if errors.Is(err, sql.ErrNoRows) { return false, nil } @@ -59,17 +56,13 @@ func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken str return false, err } - if sharing != nil && (sharing.ID == rootID && sharing.Enabled && sharing.Token == readToken) { + if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) { return true, nil } return false, nil } -func (a *Auth) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool { - hasAccess, err := a.store.HasWorkspaceAccess(userID, workspaceID) - if err != nil { - return false - } - return hasAccess +func (a *Auth) DoesUserHaveTeamAccess(userID string, teamID string) bool { + return a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) } diff --git a/server/auth/auth_test.go b/server/auth/auth_test.go index 8765b317b..04b6b0218 100644 --- a/server/auth/auth_test.go +++ b/server/auth/auth_test.go @@ -1,15 +1,16 @@ package auth import ( - "database/sql" "testing" "github.com/golang/mock/gomock" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/config" - "github.com/mattermost/focalboard/server/services/store" + "github.com/mattermost/focalboard/server/services/permissions/localpermissions" + mockpermissions "github.com/mattermost/focalboard/server/services/permissions/mocks" "github.com/mattermost/focalboard/server/services/store/mockstore" "github.com/mattermost/focalboard/server/utils" + "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/pkg/errors" "github.com/stretchr/testify/require" ) @@ -31,14 +32,19 @@ var mockSession = &model.Session{ func setupTestHelper(t *testing.T) *TestHelper { ctrl := gomock.NewController(t) defer ctrl.Finish() + ctrlPermissions := gomock.NewController(t) + defer ctrlPermissions.Finish() cfg := config.Configuration{} mockStore := mockstore.NewMockStore(ctrl) - newAuth := New(&cfg, mockStore) + mockPermissions := mockpermissions.NewMockStore(ctrlPermissions) + logger, err := mlog.NewLogger() + require.NoError(t, err) + newAuth := New(&cfg, mockStore, localpermissions.New(mockPermissions, logger)) // called during default template setup for every test - mockStore.EXPECT().GetDefaultTemplateBlocks().AnyTimes() + mockStore.EXPECT().GetTemplateBoards(gomock.Any()).AnyTimes() mockStore.EXPECT().RemoveDefaultTemplates(gomock.Any()).AnyTimes() - mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any()).AnyTimes() return &TestHelper{ Auth: newAuth, @@ -83,55 +89,57 @@ func TestGetSession(t *testing.T) { } func TestIsValidReadToken(t *testing.T) { - th := setupTestHelper(t) + // ToDo: reimplement - validBlockID := "testBlockID" - mockContainer := store.Container{ - WorkspaceID: "testWorkspaceID", - } - validReadToken := "testReadToken" - mockSharing := model.Sharing{ - ID: "testRootID", - Enabled: true, - Token: validReadToken, - } + // th := setupTestHelper(t) - testcases := []struct { - title string - container store.Container - blockID string - readToken string - isError bool - isSuccess bool - }{ - {"fail, error GetRootID", mockContainer, "badBlock", "", true, false}, - {"fail, rootID not found", mockContainer, "goodBlockID", "", false, false}, - {"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false}, - {"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false}, - {"success", mockContainer, validBlockID, validReadToken, false, true}, - } + // validBlockID := "testBlockID" + // mockContainer := store.Container{ + // TeamID: "testTeamID", + // } + // validReadToken := "testReadToken" + // mockSharing := model.Sharing{ + // ID: "testRootID", + // Enabled: true, + // Token: validReadToken, + // } - th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block")) - th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil) - th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil) - th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2) - th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows) - th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error")) - th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2) + // testcases := []struct { + // title string + // container store.Container + // blockID string + // readToken string + // isError bool + // isSuccess bool + // }{ + // {"fail, error GetRootID", mockContainer, "badBlock", "", true, false}, + // {"fail, rootID not found", mockContainer, "goodBlockID", "", false, false}, + // {"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false}, + // {"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false}, + // {"success", mockContainer, validBlockID, validReadToken, false, true}, + // } - for _, test := range testcases { - t.Run(test.title, func(t *testing.T) { - success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken) - if test.isError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - if test.isSuccess { - require.True(t, success) - } else { - require.False(t, success) - } - }) - } + // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block")) + // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil) + // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil) + // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2) + // th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows) + // th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error")) + // th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2) + + // for _, test := range testcases { + // t.Run(test.title, func(t *testing.T) { + // success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken) + // if test.isError { + // require.Error(t, err) + // } else { + // require.NoError(t, err) + // } + // if test.isSuccess { + // require.True(t, success) + // } else { + // require.False(t, success) + // } + // }) + // } } diff --git a/server/auth/mocks/mockauth_interface.go b/server/auth/mocks/mockauth_interface.go index c6ef2e103..98fd54c63 100644 --- a/server/auth/mocks/mockauth_interface.go +++ b/server/auth/mocks/mockauth_interface.go @@ -9,7 +9,6 @@ import ( gomock "github.com/golang/mock/gomock" model "github.com/mattermost/focalboard/server/model" - store "github.com/mattermost/focalboard/server/services/store" ) // MockAuthInterface is a mock of AuthInterface interface. @@ -35,18 +34,18 @@ func (m *MockAuthInterface) EXPECT() *MockAuthInterfaceMockRecorder { return m.recorder } -// DoesUserHaveWorkspaceAccess mocks base method. -func (m *MockAuthInterface) DoesUserHaveWorkspaceAccess(arg0, arg1 string) bool { +// DoesUserHaveTeamAccess mocks base method. +func (m *MockAuthInterface) DoesUserHaveTeamAccess(arg0, arg1 string) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DoesUserHaveWorkspaceAccess", arg0, arg1) + ret := m.ctrl.Call(m, "DoesUserHaveTeamAccess", arg0, arg1) ret0, _ := ret[0].(bool) return ret0 } -// DoesUserHaveWorkspaceAccess indicates an expected call of DoesUserHaveWorkspaceAccess. -func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveWorkspaceAccess(arg0, arg1 interface{}) *gomock.Call { +// DoesUserHaveTeamAccess indicates an expected call of DoesUserHaveTeamAccess. +func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveTeamAccess(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveWorkspaceAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveWorkspaceAccess), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveTeamAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveTeamAccess), arg0, arg1) } // GetSession mocks base method. @@ -65,16 +64,16 @@ func (mr *MockAuthInterfaceMockRecorder) GetSession(arg0 interface{}) *gomock.Ca } // IsValidReadToken mocks base method. -func (m *MockAuthInterface) IsValidReadToken(arg0 store.Container, arg1, arg2 string) (bool, error) { +func (m *MockAuthInterface) IsValidReadToken(arg0, arg1 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // IsValidReadToken indicates an expected call of IsValidReadToken. -func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1) } diff --git a/server/client/client.go b/server/client/client.go index a9023da02..a471d72d7 100644 --- a/server/client/client.go +++ b/server/client/client.go @@ -101,8 +101,8 @@ func (c *Client) DoAPIPut(url, data string) (*http.Response, error) { return c.DoAPIRequest(http.MethodPut, c.APIURL+url, data, "") } -func (c *Client) DoAPIDelete(url string) (*http.Response, error) { - return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, "", "") +func (c *Client) DoAPIDelete(url string, data string) (*http.Response, error) { + return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, data, "") } func (c *Client) DoAPIRequest(method, url, data, etag string) (*http.Response, error) { @@ -152,20 +152,50 @@ func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* eta return rp, nil } -func (c *Client) GetBlocksRoute() string { - return "/workspaces/0/blocks" +func (c *Client) GetTeamRoute(teamID string) string { + return fmt.Sprintf("%s/%s", c.GetTeamsRoute(), teamID) } -func (c *Client) GetBlockRoute(id string) string { - return fmt.Sprintf("%s/%s", c.GetBlocksRoute(), id) +func (c *Client) GetTeamsRoute() string { + return "/teams" } -func (c *Client) GetSubtreeRoute(id string) string { - return fmt.Sprintf("%s/subtree", c.GetBlockRoute(id)) +func (c *Client) GetBlockRoute(boardID, blockID string) string { + return fmt.Sprintf("%s/%s", c.GetBlocksRoute(boardID), blockID) } -func (c *Client) GetBlocks() ([]model.Block, *Response) { - r, err := c.DoAPIGet(c.GetBlocksRoute(), "") +func (c *Client) GetSubtreeRoute(boardID, blockID string) string { + return fmt.Sprintf("%s/subtree", c.GetBlockRoute(boardID, blockID)) +} + +func (c *Client) GetBoardsRoute() string { + return "/boards" +} + +func (c *Client) GetBoardRoute(boardID string) string { + return fmt.Sprintf("%s/%s", c.GetBoardsRoute(), boardID) +} + +func (c *Client) GetBlocksRoute(boardID string) string { + return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID)) +} + +func (c *Client) GetBoardsAndBlocksRoute() string { + return "/boards-and-blocks" +} + +func (c *Client) GetTeam(teamID string) (*model.Team, *Response) { + r, err := c.DoAPIGet(c.GetTeamRoute(teamID), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.TeamFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) GetBlocksForBoard(boardID string) ([]model.Block, *Response) { + r, err := c.DoAPIGet(c.GetBlocksRoute(boardID), "") if err != nil { return nil, BuildErrorResponse(r, err) } @@ -174,8 +204,8 @@ func (c *Client) GetBlocks() ([]model.Block, *Response) { return model.BlocksFromJSON(r.Body), BuildResponse(r) } -func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool, *Response) { - r, err := c.DoAPIPatch(c.GetBlockRoute(blockID), toJSON(blockPatch)) +func (c *Client) PatchBlock(boardID, blockID string, blockPatch *model.BlockPatch) (bool, *Response) { + r, err := c.DoAPIPatch(c.GetBlockRoute(boardID, blockID), toJSON(blockPatch)) if err != nil { return false, BuildErrorResponse(r, err) } @@ -184,8 +214,46 @@ func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool, return true, BuildResponse(r) } -func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) { - r, err := c.DoAPIPost(c.GetBlocksRoute(), toJSON(blocks)) +func (c *Client) DuplicateBoard(boardID string, asTemplate bool, teamID string) (bool, *Response) { + queryParams := "?asTemplate=false&" + if asTemplate { + queryParams = "?asTemplate=true" + } + r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/duplicate"+queryParams, "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + +func (c *Client) DuplicateBlock(boardID, blockID string, asTemplate bool) (bool, *Response) { + queryParams := "?asTemplate=false" + if asTemplate { + queryParams = "?asTemplate=true" + } + r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/duplicate"+queryParams, "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + +func (c *Client) UndeleteBlock(boardID, blockID string) (bool, *Response) { + r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/undelete", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + +func (c *Client) InsertBlocks(boardID string, blocks []model.Block) ([]model.Block, *Response) { + r, err := c.DoAPIPost(c.GetBlocksRoute(boardID), toJSON(blocks)) if err != nil { return nil, BuildErrorResponse(r, err) } @@ -194,8 +262,8 @@ func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) { return model.BlocksFromJSON(r.Body), BuildResponse(r) } -func (c *Client) DeleteBlock(blockID string) (bool, *Response) { - r, err := c.DoAPIDelete(c.GetBlockRoute(blockID)) +func (c *Client) DeleteBlock(boardID, blockID string) (bool, *Response) { + r, err := c.DoAPIDelete(c.GetBlockRoute(boardID, blockID), "") if err != nil { return false, BuildErrorResponse(r, err) } @@ -204,18 +272,8 @@ func (c *Client) DeleteBlock(blockID string) (bool, *Response) { return true, BuildResponse(r) } -func (c *Client) UndeleteBlock(blockID string) (bool, *Response) { - r, err := c.DoAPIPost(c.GetBlockRoute(blockID)+"/undelete", "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) { - r, err := c.DoAPIGet(c.GetSubtreeRoute(blockID), "") +func (c *Client) GetSubtree(boardID, blockID string) ([]model.Block, *Response) { + r, err := c.DoAPIGet(c.GetSubtreeRoute(boardID, blockID), "") if err != nil { return nil, BuildErrorResponse(r, err) } @@ -224,14 +282,45 @@ func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) { return model.BlocksFromJSON(r.Body), BuildResponse(r) } +// Boards and blocks. +func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) { + r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks) (*model.BoardsAndBlocks, *Response) { + r, err := c.DoAPIPatch(c.GetBoardsAndBlocksRoute(), toJSON(pbab)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks) (bool, *Response) { + r, err := c.DoAPIDelete(c.GetBoardsAndBlocksRoute(), toJSON(dbab)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + // Sharing -func (c *Client) GetSharingRoute(rootID string) string { - return fmt.Sprintf("/workspaces/0/sharing/%s", rootID) +func (c *Client) GetSharingRoute(boardID string) string { + return fmt.Sprintf("%s/sharing", c.GetBoardRoute(boardID)) } -func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) { - r, err := c.DoAPIGet(c.GetSharingRoute(rootID), "") +func (c *Client) GetSharing(boardID string) (*model.Sharing, *Response) { + r, err := c.DoAPIGet(c.GetSharingRoute(boardID), "") if err != nil { return nil, BuildErrorResponse(r, err) } @@ -241,7 +330,7 @@ func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) { return &sharing, BuildResponse(r) } -func (c *Client) PostSharing(sharing model.Sharing) (bool, *Response) { +func (c *Client) PostSharing(sharing *model.Sharing) (bool, *Response) { r, err := c.DoAPIPost(c.GetSharingRoute(sharing.ID), toJSON(sharing)) if err != nil { return false, BuildErrorResponse(r, err) @@ -338,11 +427,116 @@ func (c *Client) UserChangePassword(id string, data *api.ChangePasswordRequest) return true, BuildResponse(r) } -func (c *Client) GetWorkspaceUploadFileRoute(workspaceID, rootID string) string { - return fmt.Sprintf("/workspaces/%s/%s/files", workspaceID, rootID) +func (c *Client) CreateBoard(board *model.Board) (*model.Board, *Response) { + r, err := c.DoAPIPost(c.GetBoardsRoute(), toJSON(board)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardFromJSON(r.Body), BuildResponse(r) } -func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) (*api.FileUploadResponse, *Response) { +func (c *Client) PatchBoard(boardID string, patch *model.BoardPatch) (*model.Board, *Response) { + r, err := c.DoAPIPatch(c.GetBoardRoute(boardID), toJSON(patch)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) DeleteBoard(boardID string) (bool, *Response) { + r, err := c.DoAPIDelete(c.GetBoardRoute(boardID), "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + +func (c *Client) GetBoard(boardID, readToken string) (*model.Board, *Response) { + url := c.GetBoardRoute(boardID) + if readToken != "" { + url += fmt.Sprintf("?read_token=%s", readToken) + } + + r, err := c.DoAPIGet(url, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) GetBoardsForTeam(teamID string) ([]*model.Board, *Response) { + r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardsFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) SearchBoardsForTeam(teamID, term string) ([]*model.Board, *Response) { + r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards/search?q="+term, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardsFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) GetMembersForBoard(boardID string) ([]*model.BoardMember, *Response) { + r, err := c.DoAPIGet(c.GetBoardRoute(boardID)+"/members", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardMembersFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, *Response) { + r, err := c.DoAPIPost(c.GetBoardRoute(member.BoardID)+"/members", toJSON(member)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardMemberFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) { + r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return model.BoardMemberFromJSON(r.Body), BuildResponse(r) +} + +func (c *Client) DeleteBoardMember(member *model.BoardMember) (bool, *Response) { + r, err := c.DoAPIDelete(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + +func (c *Client) GetTeamUploadFileRoute(teamID, boardID string) string { + return fmt.Sprintf("%s/%s/files", c.GetTeamRoute(teamID), boardID) +} + +func (c *Client) TeamUploadFile(teamID, boardID string, data io.Reader) (*api.FileUploadResponse, *Response) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile(api.UploadFormFileKey, "file") @@ -358,7 +552,7 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) r.Header.Add("Content-Type", writer.FormDataContentType()) } - r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetWorkspaceUploadFileRoute(workspaceID, rootID), body, "", opt) + r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetTeamUploadFileRoute(teamID, boardID), body, "", opt) if err != nil { return nil, BuildErrorResponse(r, err) } @@ -372,12 +566,12 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) return fileUploadResponse, BuildResponse(r) } -func (c *Client) GetSubscriptionsRoute(workspaceID string) string { - return fmt.Sprintf("/workspaces/%s/subscriptions", workspaceID) +func (c *Client) GetSubscriptionsRoute() string { + return "/subscriptions" } -func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription) (*model.Subscription, *Response) { - r, err := c.DoAPIPost(c.GetSubscriptionsRoute(workspaceID), toJSON(&sub)) +func (c *Client) CreateSubscription(sub *model.Subscription) (*model.Subscription, *Response) { + r, err := c.DoAPIPost(c.GetSubscriptionsRoute(), toJSON(&sub)) if err != nil { return nil, BuildErrorResponse(r, err) } @@ -390,10 +584,10 @@ func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription) return subNew, BuildResponse(r) } -func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscriberID string) *Response { - url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(workspaceID), blockID, subscriberID) +func (c *Client) DeleteSubscription(blockID string, subscriberID string) *Response { + url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(), blockID, subscriberID) - r, err := c.DoAPIDelete(url) + r, err := c.DoAPIDelete(url, "") if err != nil { return BuildErrorResponse(r, err) } @@ -402,8 +596,8 @@ func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscrib return BuildResponse(r) } -func (c *Client) GetSubscriptions(workspaceID string, subscriberID string) ([]*model.Subscription, *Response) { - url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(workspaceID), subscriberID) +func (c *Client) GetSubscriptions(subscriberID string) ([]*model.Subscription, *Response) { + url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(), subscriberID) r, err := c.DoAPIGet(url, "") if err != nil { diff --git a/server/go.mod b/server/go.mod index ac3d960c3..5cd167f19 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,7 +5,6 @@ go 1.16 require ( github.com/Masterminds/squirrel v1.5.0 github.com/go-sql-driver/mysql v1.6.0 - github.com/golang-migrate/migrate/v4 v4.14.1 github.com/golang/mock v1.5.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 @@ -14,6 +13,7 @@ require ( github.com/magiconair/properties v1.8.5 // indirect github.com/mattermost/mattermost-plugin-api v0.0.21 github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0 + github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/oklog/run v1.1.0 diff --git a/server/go.sum b/server/go.sum index 52ef7429a..56a326eaf 100644 --- a/server/go.sum +++ b/server/go.sum @@ -47,7 +47,6 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -68,7 +67,6 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk= @@ -161,7 +159,6 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY= github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -185,6 +182,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1 github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -195,19 +193,14 @@ github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFau github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dhui/dktest v0.3.3 h1:DBuH/9GFaWbDRa42qsut/hbQu+srAQ0rPWnUoiGX7CA= github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ= github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8= github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= @@ -233,6 +226,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= @@ -290,9 +284,7 @@ github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhD github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE= github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -346,6 +338,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= @@ -366,8 +359,9 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -403,7 +397,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -416,7 +409,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0= github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= @@ -513,6 +505,8 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -564,6 +558,7 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -592,6 +587,8 @@ github.com/mattermost/mattermost-plugin-api v0.0.21/go.mod h1:qz19Y+5HLbjtzY2RZ6 github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go.mod h1:kmxJuVgpX13Th+e5L1ZsBs4aq+ETmmDg9joo5r4cIw8= github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0 h1:A7TCgCGF9JmAHBQv9qGm5SfPYTAl8dOXy/u6lCSV8ow= github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08= +github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14= +github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -616,6 +613,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -658,7 +656,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= @@ -705,9 +702,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= @@ -782,6 +777,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1063,6 +1059,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1203,6 +1200,7 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1213,8 +1211,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1287,9 +1287,11 @@ golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1454,17 +1456,139 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= +modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM= +modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= +modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= +modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc= +modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8= +modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= +modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q= +modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= +modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ= +modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y= modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw= +modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk= +modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/server/integrationtests/blocks_test.go b/server/integrationtests/blocks_test.go index 247caa9c8..573bd1144 100644 --- a/server/integrationtests/blocks_test.go +++ b/server/integrationtests/blocks_test.go @@ -11,40 +11,38 @@ import ( ) func TestGetBlocks(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelperWithToken(t).Start() defer th.TearDown() - blocks, resp := th.Client.GetBlocks() - require.NoError(t, resp.Error) - initialCount := len(blocks) + board := th.CreateBoard("team-id", model.BoardTypeOpen) initialID1 := utils.NewID(utils.IDTypeBlock) initialID2 := utils.NewID(utils.IDTypeBlock) newBlocks := []model.Block{ { ID: initialID1, - RootID: initialID1, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, }, { ID: initialID2, - RootID: initialID2, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, }, } - newBlocks, resp = th.Client.InsertBlocks(newBlocks) + newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks) require.NoError(t, resp.Error) require.Len(t, newBlocks, 2) blockID1 := newBlocks[0].ID blockID2 := newBlocks[1].ID - blocks, resp = th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+2) + require.Len(t, blocks, 2) blockIDs := make([]string, len(blocks)) for i, b := range blocks { @@ -55,12 +53,10 @@ func TestGetBlocks(t *testing.T) { } func TestPostBlock(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelperWithToken(t).Start() defer th.TearDown() - blocks, resp := th.Client.GetBlocks() - require.NoError(t, resp.Error) - initialCount := len(blocks) + board := th.CreateBoard("team-id", model.BoardTypeOpen) var blockID1 string var blockID2 string @@ -70,21 +66,21 @@ func TestPostBlock(t *testing.T) { initialID1 := utils.NewID(utils.IDTypeBlock) block := model.Block{ ID: initialID1, - RootID: initialID1, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, Title: "New title", } - newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) + newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) require.NoError(t, resp.Error) require.Len(t, newBlocks, 1) blockID1 = newBlocks[0].ID - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+1) + require.Len(t, blocks, 1) blockIDs := make([]string, len(blocks)) for i, b := range blocks { @@ -99,21 +95,21 @@ func TestPostBlock(t *testing.T) { newBlocks := []model.Block{ { ID: initialID2, - RootID: initialID2, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, }, { ID: initialID3, - RootID: initialID3, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, }, } - newBlocks, resp := th.Client.InsertBlocks(newBlocks) + newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks) require.NoError(t, resp.Error) require.Len(t, newBlocks, 2) blockID2 = newBlocks[0].ID @@ -121,9 +117,9 @@ func TestPostBlock(t *testing.T) { require.NotEqual(t, initialID2, blockID2) require.NotEqual(t, initialID3, blockID3) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+3) + require.Len(t, blocks, 3) blockIDs := make([]string, len(blocks)) for i, b := range blocks { @@ -137,22 +133,22 @@ func TestPostBlock(t *testing.T) { t.Run("Update a block should not be possible through the insert endpoint", func(t *testing.T) { block := model.Block{ ID: blockID1, - RootID: blockID1, + BoardID: board.ID, CreateAt: 1, UpdateAt: 20, - Type: model.TypeBoard, + Type: model.TypeCard, Title: "Updated title", } - newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) + newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) require.NoError(t, resp.Error) require.Len(t, newBlocks, 1) blockID4 := newBlocks[0].ID require.NotEqual(t, blockID1, blockID4) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+4) + require.Len(t, blocks, 4) var block4 model.Block for _, b := range blocks { @@ -166,42 +162,41 @@ func TestPostBlock(t *testing.T) { } func TestPatchBlock(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelperWithToken(t).Start() defer th.TearDown() initialID := utils.NewID(utils.IDTypeBlock) + board := th.CreateBoard("team-id", model.BoardTypeOpen) + time.Sleep(10 * time.Millisecond) + block := model.Block{ ID: initialID, - RootID: initialID, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, Title: "New title", Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"}, } - newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) - require.NoError(t, resp.Error) + newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) + th.CheckOK(resp) require.Len(t, newBlocks, 1) blockID := newBlocks[0].ID - blocks, resp := th.Client.GetBlocks() - require.NoError(t, resp.Error) - initialCount := len(blocks) - t.Run("Patch a block basic field", func(t *testing.T) { newTitle := "Updated title" blockPatch := &model.BlockPatch{ Title: &newTitle, } - _, resp := th.Client.PatchBlock(blockID, blockPatch) + _, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch) require.NoError(t, resp.Error) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount) + require.Len(t, blocks, 1) var updatedBlock model.Block for _, b := range blocks { @@ -221,12 +216,12 @@ func TestPatchBlock(t *testing.T) { }, } - _, resp := th.Client.PatchBlock(blockID, blockPatch) + _, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch) require.NoError(t, resp.Error) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount) + require.Len(t, blocks, 1) var updatedBlock model.Block for _, b := range blocks { @@ -244,12 +239,12 @@ func TestPatchBlock(t *testing.T) { DeletedFields: []string{"test", "test3", "test100"}, } - _, resp := th.Client.PatchBlock(blockID, blockPatch) + _, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch) require.NoError(t, resp.Error) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount) + require.Len(t, blocks, 1) var updatedBlock model.Block for _, b := range blocks { @@ -265,35 +260,34 @@ func TestPatchBlock(t *testing.T) { } func TestDeleteBlock(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelperWithToken(t).Start() defer th.TearDown() - blocks, resp := th.Client.GetBlocks() - require.NoError(t, resp.Error) - initialCount := len(blocks) + board := th.CreateBoard("team-id", model.BoardTypeOpen) + time.Sleep(10 * time.Millisecond) var blockID string t.Run("Create a block", func(t *testing.T) { initialID := utils.NewID(utils.IDTypeBlock) block := model.Block{ ID: initialID, - RootID: initialID, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, Title: "New title", } - newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) + newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) require.NoError(t, resp.Error) require.Len(t, newBlocks, 1) require.NotZero(t, newBlocks[0].ID) require.NotEqual(t, initialID, newBlocks[0].ID) blockID = newBlocks[0].ID - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+1) + require.Len(t, blocks, 1) blockIDs := make([]string, len(blocks)) for i, b := range blocks { @@ -307,43 +301,45 @@ func TestDeleteBlock(t *testing.T) { // id,insert_at on block history time.Sleep(10 * time.Millisecond) - _, resp := th.Client.DeleteBlock(blockID) + _, resp := th.Client.DeleteBlock(board.ID, blockID) require.NoError(t, resp.Error) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount) + require.Empty(t, blocks) }) } func TestUndeleteBlock(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelper(t).InitBasic() defer th.TearDown() - blocks, resp := th.Client.GetBlocks() + board := th.CreateBoard("team-id", model.BoardTypeOpen) + + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) initialCount := len(blocks) var blockID string t.Run("Create a block", func(t *testing.T) { - initialID := utils.NewID(utils.IDTypeBlock) + initialID := utils.NewID(utils.IDTypeBoard) block := model.Block{ ID: initialID, - RootID: initialID, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, Type: model.TypeBoard, Title: "New title", } - newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) + newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) require.NoError(t, resp.Error) require.Len(t, newBlocks, 1) require.NotZero(t, newBlocks[0].ID) require.NotEqual(t, initialID, newBlocks[0].ID) blockID = newBlocks[0].ID - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) require.Len(t, blocks, initialCount+1) @@ -359,10 +355,10 @@ func TestUndeleteBlock(t *testing.T) { // id,insert_at on block history time.Sleep(10 * time.Millisecond) - _, resp := th.Client.DeleteBlock(blockID) + _, resp := th.Client.DeleteBlock(board.ID, blockID) require.NoError(t, resp.Error) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) require.Len(t, blocks, initialCount) }) @@ -372,10 +368,10 @@ func TestUndeleteBlock(t *testing.T) { // id,insert_at on block history time.Sleep(10 * time.Millisecond) - _, resp := th.Client.UndeleteBlock(blockID) + _, resp := th.Client.UndeleteBlock(board.ID, blockID) require.NoError(t, resp.Error) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) require.Len(t, blocks, initialCount+1) }) @@ -384,12 +380,10 @@ func TestUndeleteBlock(t *testing.T) { func TestGetSubtree(t *testing.T) { t.Skip("TODO: fix flaky test") - th := SetupTestHelper().InitBasic() + th := SetupTestHelperWithToken(t).Start() defer th.TearDown() - blocks, resp := th.Client.GetBlocks() - require.NoError(t, resp.Error) - initialCount := len(blocks) + board := th.CreateBoard("team-id", model.BoardTypeOpen) parentBlockID := utils.NewID(utils.IDTypeBlock) childBlockID1 := utils.NewID(utils.IDTypeBlock) @@ -399,14 +393,14 @@ func TestGetSubtree(t *testing.T) { newBlocks := []model.Block{ { ID: parentBlockID, - RootID: parentBlockID, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, + Type: model.TypeCard, }, { ID: childBlockID1, - RootID: parentBlockID, + BoardID: board.ID, ParentID: parentBlockID, CreateAt: 2, UpdateAt: 2, @@ -414,7 +408,7 @@ func TestGetSubtree(t *testing.T) { }, { ID: childBlockID2, - RootID: parentBlockID, + BoardID: board.ID, ParentID: parentBlockID, CreateAt: 2, UpdateAt: 2, @@ -422,12 +416,12 @@ func TestGetSubtree(t *testing.T) { }, } - _, resp := th.Client.InsertBlocks(newBlocks) + _, resp := th.Client.InsertBlocks(board.ID, newBlocks) require.NoError(t, resp.Error) - blocks, resp := th.Client.GetBlocks() + blocks, resp := th.Client.GetBlocksForBoard(board.ID) require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+1) // GetBlocks returns root blocks (null ParentID) + require.Len(t, blocks, 1) // GetBlocks returns root blocks (null ParentID) blockIDs := make([]string, len(blocks)) for i, b := range blocks { @@ -437,7 +431,7 @@ func TestGetSubtree(t *testing.T) { }) t.Run("Get subtree for parent ID", func(t *testing.T) { - blocks, resp := th.Client.GetSubtree(parentBlockID) + blocks, resp := th.Client.GetSubtree(board.ID, parentBlockID) require.NoError(t, resp.Error) require.Len(t, blocks, 3) diff --git a/server/integrationtests/board_test.go b/server/integrationtests/board_test.go new file mode 100644 index 000000000..d61650151 --- /dev/null +++ b/server/integrationtests/board_test.go @@ -0,0 +1,1270 @@ +package integrationtests + +import ( + "testing" + + "github.com/mattermost/focalboard/server/client" + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/utils" + + "github.com/stretchr/testify/require" +) + +const ( + testTeamID = "team-id" +) + +func TestGetBoards(t *testing.T) { + t.Run("a non authenticated client should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Logout(th.Client) + + teamID := "0" + newBoard := &model.Board{ + TeamID: teamID, + Type: model.BoardTypeOpen, + } + + board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) + require.NoError(t, err) + require.NotNil(t, board) + + boards, resp := th.Client.GetBoardsForTeam(teamID) + th.CheckUnauthorized(resp) + require.Nil(t, boards) + }) + + t.Run("should only return the boards that the user is a member of", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + teamID := "0" + otherTeamID := "other-team-id" + user1 := th.GetUser1() + + board1 := &model.Board{ + TeamID: teamID, + Type: model.BoardTypeOpen, + } + rBoard1, err := th.Server.App().CreateBoard(board1, user1.ID, true) + require.NoError(t, err) + require.NotNil(t, rBoard1) + + board2 := &model.Board{ + TeamID: teamID, + Type: model.BoardTypeOpen, + } + rBoard2, err := th.Server.App().CreateBoard(board2, user1.ID, false) + require.NoError(t, err) + require.NotNil(t, rBoard2) + + board3 := &model.Board{ + TeamID: teamID, + Type: model.BoardTypePrivate, + } + rBoard3, err := th.Server.App().CreateBoard(board3, user1.ID, true) + require.NoError(t, err) + require.NotNil(t, rBoard3) + + board4 := &model.Board{ + TeamID: teamID, + Type: model.BoardTypePrivate, + } + rBoard4, err := th.Server.App().CreateBoard(board4, user1.ID, false) + require.NoError(t, err) + require.NotNil(t, rBoard4) + + board5 := &model.Board{ + TeamID: otherTeamID, + Type: model.BoardTypeOpen, + } + rBoard5, err := th.Server.App().CreateBoard(board5, user1.ID, true) + require.NoError(t, err) + require.NotNil(t, rBoard5) + + boards, resp := th.Client.GetBoardsForTeam(teamID) + th.CheckOK(resp) + require.NotNil(t, boards) + require.Len(t, boards, 2) + + boardIDs := []string{} + for _, board := range boards { + boardIDs = append(boardIDs, board.ID) + } + require.ElementsMatch(t, []string{rBoard1.ID, rBoard3.ID}, boardIDs) + + boardsFromOtherTeam, resp := th.Client.GetBoardsForTeam(otherTeamID) + th.CheckOK(resp) + require.NotNil(t, boardsFromOtherTeam) + require.Len(t, boardsFromOtherTeam, 1) + require.Equal(t, rBoard5.ID, boardsFromOtherTeam[0].ID) + }) +} + +func TestCreateBoard(t *testing.T) { + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Logout(th.Client) + + newBoard := &model.Board{ + Title: "board title", + Type: model.BoardTypeOpen, + TeamID: testTeamID, + } + board, resp := th.Client.CreateBoard(newBoard) + th.CheckUnauthorized(resp) + require.Nil(t, board) + }) + + t.Run("create public board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + me := th.GetUser1() + + title := "board title 1" + teamID := testTeamID + newBoard := &model.Board{ + Title: title, + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, resp := th.Client.CreateBoard(newBoard) + th.CheckOK(resp) + require.NoError(t, resp.Error) + require.NotNil(t, board) + require.NotNil(t, board.ID) + require.Equal(t, title, board.Title) + require.Equal(t, model.BoardTypeOpen, board.Type) + require.Equal(t, teamID, board.TeamID) + require.Equal(t, me.ID, board.CreatedBy) + require.Equal(t, me.ID, board.ModifiedBy) + + t.Run("creating a board should make the creator an admin", func(t *testing.T) { + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + require.Equal(t, me.ID, members[0].UserID) + require.Equal(t, board.ID, members[0].BoardID) + require.True(t, members[0].SchemeAdmin) + }) + }) + + t.Run("create private board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + me := th.GetUser1() + + title := "board title" + teamID := testTeamID + newBoard := &model.Board{ + Title: title, + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, resp := th.Client.CreateBoard(newBoard) + th.CheckOK(resp) + require.NotNil(t, board) + require.NotNil(t, board.ID) + require.Equal(t, title, board.Title) + require.Equal(t, model.BoardTypePrivate, board.Type) + require.Equal(t, teamID, board.TeamID) + require.Equal(t, me.ID, board.CreatedBy) + require.Equal(t, me.ID, board.ModifiedBy) + + t.Run("creating a board should make the creator an admin", func(t *testing.T) { + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + require.Equal(t, me.ID, members[0].UserID) + require.Equal(t, board.ID, members[0].BoardID) + require.True(t, members[0].SchemeAdmin) + }) + }) + + t.Run("create invalid board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + title := "board title" + teamID := testTeamID + user1 := th.GetUser1() + + t.Run("invalid board type", func(t *testing.T) { + var invalidBoardType model.BoardType = "invalid" + newBoard := &model.Board{ + Title: title, + TeamID: testTeamID, + Type: invalidBoardType, + } + + board, resp := th.Client.CreateBoard(newBoard) + th.CheckBadRequest(resp) + require.Nil(t, board) + + boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID) + require.NoError(t, err) + require.Empty(t, boards) + }) + + t.Run("no type", func(t *testing.T) { + newBoard := &model.Board{ + Title: title, + TeamID: teamID, + } + board, resp := th.Client.CreateBoard(newBoard) + th.CheckBadRequest(resp) + require.Nil(t, board) + + boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID) + require.NoError(t, err) + require.Empty(t, boards) + }) + + t.Run("no team ID", func(t *testing.T) { + newBoard := &model.Board{ + Title: title, + } + board, resp := th.Client.CreateBoard(newBoard) + // the request is unauthorized because the permission + // check fails on an empty teamID + th.CheckForbidden(resp) + require.Nil(t, board) + + boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID) + require.NoError(t, err) + require.Empty(t, boards) + }) + }) +} + +func TestSearchBoards(t *testing.T) { + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Logout(th.Client) + + boards, resp := th.Client.SearchBoardsForTeam(testTeamID, "term") + th.CheckUnauthorized(resp) + require.Nil(t, boards) + }) + + t.Run("all the matching private boards that the user is a member of and all matching public boards should be returned", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + teamID := testTeamID + user1 := th.GetUser1() + + board1 := &model.Board{ + Title: "public board where user1 is admin", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + rBoard1, err := th.Server.App().CreateBoard(board1, user1.ID, true) + require.NoError(t, err) + + board2 := &model.Board{ + Title: "public board where user1 is not member", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + rBoard2, err := th.Server.App().CreateBoard(board2, user1.ID, false) + require.NoError(t, err) + + board3 := &model.Board{ + Title: "private board where user1 is admin", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + rBoard3, err := th.Server.App().CreateBoard(board3, user1.ID, true) + require.NoError(t, err) + + board4 := &model.Board{ + Title: "private board where user1 is not member", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + _, err = th.Server.App().CreateBoard(board4, user1.ID, false) + require.NoError(t, err) + + board5 := &model.Board{ + Title: "public board where user1 is admin, but in other team", + Type: model.BoardTypePrivate, + TeamID: "other-team-id", + } + _, err = th.Server.App().CreateBoard(board5, user1.ID, true) + require.NoError(t, err) + + testCases := []struct { + Name string + Client *client.Client + Term string + ExpectedIDs []string + }{ + { + Name: "should return all boards where user1 is member or that are public", + Client: th.Client, + Term: "board", + ExpectedIDs: []string{rBoard1.ID, rBoard2.ID, rBoard3.ID}, + }, + { + Name: "matching a full word", + Client: th.Client, + Term: "admin", + ExpectedIDs: []string{rBoard1.ID, rBoard3.ID}, + }, + { + Name: "matching part of the word", + Client: th.Client, + Term: "ubli", + ExpectedIDs: []string{rBoard1.ID, rBoard2.ID}, + }, + { + Name: "case insensitive", + Client: th.Client, + Term: "UBLI", + ExpectedIDs: []string{rBoard1.ID, rBoard2.ID}, + }, + { + Name: "user2 can only see the public boards, as he's not a member of any", + Client: th.Client2, + Term: "board", + ExpectedIDs: []string{rBoard1.ID, rBoard2.ID}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + boards, resp := tc.Client.SearchBoardsForTeam(teamID, tc.Term) + th.CheckOK(resp) + + boardIDs := []string{} + for _, board := range boards { + boardIDs = append(boardIDs, board.ID) + } + + require.ElementsMatch(t, tc.ExpectedIDs, boardIDs) + }) + } + }) +} + +func TestGetBoard(t *testing.T) { + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Logout(th.Client) + + board, resp := th.Client.GetBoard("boar-id", "") + th.CheckUnauthorized(resp) + require.Nil(t, board) + }) + + t.Run("valid read token should be enough to get the board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Server.Config().EnablePublicSharedBoards = true + + teamID := testTeamID + sharingToken := utils.NewID(utils.IDTypeToken) + + board := &model.Board{ + Title: "public board where user1 is admin", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + rBoard, err := th.Server.App().CreateBoard(board, th.GetUser1().ID, true) + require.NoError(t, err) + + sharing := &model.Sharing{ + ID: rBoard.ID, + Enabled: true, + Token: sharingToken, + UpdateAt: 1, + } + + success, resp := th.Client.PostSharing(sharing) + th.CheckOK(resp) + require.True(t, success) + + // the client logs out + th.Logout(th.Client) + + // we make sure that the client cannot currently retrieve the + // board with no session + board, resp = th.Client.GetBoard(rBoard.ID, "") + th.CheckUnauthorized(resp) + require.Nil(t, board) + + // it should be able to retrieve it with the read token + board, resp = th.Client.GetBoard(rBoard.ID, sharingToken) + th.CheckOK(resp) + require.NotNil(t, board) + }) + + t.Run("nonexisting board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + board, resp := th.Client.GetBoard("nonexistent board", "") + th.CheckNotFound(resp) + require.Nil(t, board) + }) + + t.Run("a user that doesn't have permissions to a private board cannot retrieve it", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + teamID := testTeamID + newBoard := &model.Board{ + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, false) + require.NoError(t, err) + + rBoard, resp := th.Client.GetBoard(board.ID, "") + th.CheckForbidden(resp) + require.Nil(t, rBoard) + }) + + t.Run("a user that has permissions to a private board can retrieve it", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + teamID := testTeamID + newBoard := &model.Board{ + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + rBoard, resp := th.Client.GetBoard(board.ID, "") + th.CheckOK(resp) + require.NotNil(t, rBoard) + }) + + t.Run("a user that doesn't have permissions to a public board but have them to its team can retrieve it", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + teamID := testTeamID + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, false) + require.NoError(t, err) + + rBoard, resp := th.Client.GetBoard(board.ID, "") + th.CheckOK(resp) + require.NotNil(t, rBoard) + }) +} + +func TestPatchBoard(t *testing.T) { + teamID := testTeamID + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Logout(th.Client) + + initialTitle := "title 1" + newBoard := &model.Board{ + Title: initialTitle, + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) + require.NoError(t, err) + + newTitle := "a new title 1" + patch := &model.BoardPatch{Title: &newTitle} + + rBoard, resp := th.Client.PatchBoard(board.ID, patch) + th.CheckUnauthorized(resp) + require.Nil(t, rBoard) + + dbBoard, err := th.Server.App().GetBoard(board.ID) + require.NoError(t, err) + require.Equal(t, initialTitle, dbBoard.Title) + }) + + t.Run("non existing board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newTitle := "a new title 2" + patch := &model.BoardPatch{Title: &newTitle} + + board, resp := th.Client.PatchBoard("non-existing-board", patch) + th.CheckNotFound(resp) + require.Nil(t, board) + }) + + t.Run("invalid patch on a board with permissions", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + user1 := th.GetUser1() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, user1.ID, true) + require.NoError(t, err) + + var invalidPatchType model.BoardType = "invalid" + patch := &model.BoardPatch{Type: &invalidPatchType} + + rBoard, resp := th.Client.PatchBoard(board.ID, patch) + th.CheckBadRequest(resp) + require.Nil(t, rBoard) + }) + + t.Run("valid patch on a board with permissions", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + user1 := th.GetUser1() + + initialTitle := "title" + newBoard := &model.Board{ + Title: initialTitle, + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, user1.ID, true) + require.NoError(t, err) + + newTitle := "a new title" + patch := &model.BoardPatch{Title: &newTitle} + + rBoard, resp := th.Client.PatchBoard(board.ID, patch) + th.CheckOK(resp) + require.NotNil(t, rBoard) + require.Equal(t, newTitle, rBoard.Title) + }) + + t.Run("valid patch on a board without permissions", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + user1 := th.GetUser1() + + initialTitle := "title" + newBoard := &model.Board{ + Title: initialTitle, + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, user1.ID, false) + require.NoError(t, err) + + newTitle := "a new title" + patch := &model.BoardPatch{Title: &newTitle} + + rBoard, resp := th.Client.PatchBoard(board.ID, patch) + th.CheckForbidden(resp) + require.Nil(t, rBoard) + + dbBoard, err := th.Server.App().GetBoard(board.ID) + require.NoError(t, err) + require.Equal(t, initialTitle, dbBoard.Title) + }) +} + +func TestDeleteBoard(t *testing.T) { + teamID := testTeamID + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Logout(th.Client) + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) + require.NoError(t, err) + + success, resp := th.Client.DeleteBoard(board.ID) + th.CheckUnauthorized(resp) + require.False(t, success) + + dbBoard, err := th.Server.App().GetBoard(board.ID) + require.NoError(t, err) + require.NotNil(t, dbBoard) + }) + + t.Run("a user without permissions should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, "some-user-id", false) + require.NoError(t, err) + + success, resp := th.Client.DeleteBoard(board.ID) + th.CheckForbidden(resp) + require.False(t, success) + + dbBoard, err := th.Server.App().GetBoard(board.ID) + require.NoError(t, err) + require.NotNil(t, dbBoard) + }) + + t.Run("non existing board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + success, resp := th.Client.DeleteBoard("non-existing-board") + th.CheckForbidden(resp) + require.False(t, success) + }) + + t.Run("an existing board should be correctly deleted", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + success, resp := th.Client.DeleteBoard(board.ID) + th.CheckOK(resp) + require.True(t, success) + + dbBoard, err := th.Server.App().GetBoard(board.ID) + require.NoError(t, err) + require.Nil(t, dbBoard) + }) +} + +func TestGetMembersForBoard(t *testing.T) { + teamID := testTeamID + + createBoardWithUsers := func(th *TestHelper) *model.Board { + user1 := th.GetUser1() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, user1.ID, true) + require.NoError(t, err) + + newUser2Member := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeEditor: true, + } + user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) + require.NoError(t, err) + require.NotNil(t, user2Member) + + return board + } + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + board := createBoardWithUsers(th) + th.Logout(th.Client) + + members, resp := th.Client.GetMembersForBoard(board.ID) + th.CheckUnauthorized(resp) + require.Empty(t, members) + }) + + t.Run("a user without permissions should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + board := createBoardWithUsers(th) + + _ = th.Server.App().DeleteBoardMember(board.ID, th.GetUser2().ID) + + members, resp := th.Client2.GetMembersForBoard(board.ID) + th.CheckForbidden(resp) + require.Empty(t, members) + }) + + t.Run("non existing board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + members, resp := th.Client.GetMembersForBoard("non-existing-board") + th.CheckForbidden(resp) + require.Empty(t, members) + }) + + t.Run("should correctly return board members for a valid board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + board := createBoardWithUsers(th) + + members, resp := th.Client.GetMembersForBoard(board.ID) + th.CheckOK(resp) + require.Len(t, members, 2) + }) +} + +func TestAddMember(t *testing.T) { + teamID := testTeamID + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + th.Logout(th.Client) + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) + require.NoError(t, err) + + newMember := &model.BoardMember{ + UserID: "user1", + BoardID: board.ID, + SchemeEditor: true, + } + + member, resp := th.Client.AddMemberToBoard(newMember) + th.CheckUnauthorized(resp) + require.Nil(t, member) + }) + + t.Run("a user without permissions should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) + require.NoError(t, err) + + newMember := &model.BoardMember{ + UserID: "user1", + BoardID: board.ID, + SchemeEditor: true, + } + + member, resp := th.Client.AddMemberToBoard(newMember) + th.CheckForbidden(resp) + require.Nil(t, member) + }) + + t.Run("non existing board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newMember := &model.BoardMember{ + UserID: "user1", + BoardID: "non-existing-board-id", + SchemeEditor: true, + } + + member, resp := th.Client.AddMemberToBoard(newMember) + th.CheckNotFound(resp) + require.Nil(t, member) + }) + + t.Run("should correctly add a new member for a valid board", func(t *testing.T) { + t.Run("a private board through an admin user", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newMember := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeEditor: true, + } + + member, resp := th.Client.AddMemberToBoard(newMember) + th.CheckOK(resp) + require.Equal(t, newMember.UserID, member.UserID) + require.Equal(t, newMember.BoardID, member.BoardID) + require.Equal(t, newMember.SchemeAdmin, member.SchemeAdmin) + require.Equal(t, newMember.SchemeEditor, member.SchemeEditor) + require.False(t, member.SchemeCommenter) + require.False(t, member.SchemeViewer) + }) + + t.Run("a public board through a user that is not yet a member", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newMember := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeEditor: true, + } + + member, resp := th.Client2.AddMemberToBoard(newMember) + th.CheckOK(resp) + require.Equal(t, newMember.UserID, member.UserID) + require.Equal(t, newMember.BoardID, member.BoardID) + require.Equal(t, newMember.SchemeAdmin, member.SchemeAdmin) + require.Equal(t, newMember.SchemeEditor, member.SchemeEditor) + require.False(t, member.SchemeCommenter) + require.False(t, member.SchemeViewer) + + members, resp := th.Client.GetMembersForBoard(board.ID) + th.CheckOK(resp) + require.Len(t, members, 2) + }) + + t.Run("should always add a new member as an editor", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newMember := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeAdmin: true, + SchemeEditor: false, + } + + member, resp := th.Client.AddMemberToBoard(newMember) + th.CheckOK(resp) + require.Equal(t, newMember.UserID, member.UserID) + require.Equal(t, newMember.BoardID, member.BoardID) + require.False(t, member.SchemeAdmin) + require.True(t, member.SchemeEditor) + }) + }) + + t.Run("should do nothing if the member already exists", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newMember := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + SchemeAdmin: false, + SchemeEditor: true, + } + + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + require.True(t, members[0].SchemeAdmin) + require.True(t, members[0].SchemeEditor) + + member, resp := th.Client.AddMemberToBoard(newMember) + th.CheckOK(resp) + require.True(t, member.SchemeAdmin) + require.True(t, member.SchemeEditor) + + members, err = th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + require.True(t, members[0].SchemeAdmin) + require.True(t, members[0].SchemeEditor) + }) +} + +func TestUpdateMember(t *testing.T) { + teamID := testTeamID + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + updatedMember := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + SchemeEditor: true, + } + + th.Logout(th.Client) + member, resp := th.Client.UpdateBoardMember(updatedMember) + th.CheckUnauthorized(resp) + require.Nil(t, member) + }) + + t.Run("a user without permissions should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + updatedMember := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + SchemeEditor: true, + } + + member, resp := th.Client2.UpdateBoardMember(updatedMember) + th.CheckForbidden(resp) + require.Nil(t, member) + }) + + t.Run("non existing board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + updatedMember := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: "non-existent-board-id", + SchemeEditor: true, + } + + member, resp := th.Client.UpdateBoardMember(updatedMember) + th.CheckForbidden(resp) + require.Nil(t, member) + }) + + t.Run("should correctly update a member for a valid board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newUser2Member := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeEditor: true, + } + user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) + require.NoError(t, err) + require.NotNil(t, user2Member) + require.False(t, user2Member.SchemeAdmin) + require.True(t, user2Member.SchemeEditor) + + memberUpdate := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeAdmin: true, + SchemeEditor: true, + } + + updatedUser2Member, resp := th.Client.UpdateBoardMember(memberUpdate) + th.CheckOK(resp) + require.True(t, updatedUser2Member.SchemeAdmin) + require.True(t, updatedUser2Member.SchemeEditor) + }) + + t.Run("should not update a member if that means that a board will not have any admin", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + memberUpdate := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + SchemeEditor: true, + } + + updatedUser1Member, resp := th.Client.UpdateBoardMember(memberUpdate) + th.CheckBadRequest(resp) + require.Nil(t, updatedUser1Member) + + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + require.True(t, members[0].SchemeAdmin) + }) +} + +func TestDeleteMember(t *testing.T) { + teamID := testTeamID + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + member := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + } + + th.Logout(th.Client) + success, resp := th.Client.DeleteBoardMember(member) + th.CheckUnauthorized(resp) + require.False(t, success) + }) + + t.Run("a user without permissions should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + member := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + } + + success, resp := th.Client2.DeleteBoardMember(member) + th.CheckForbidden(resp) + require.False(t, success) + }) + + t.Run("non existing board", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + updatedMember := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: "non-existent-board-id", + } + + success, resp := th.Client.DeleteBoardMember(updatedMember) + th.CheckNotFound(resp) + require.False(t, success) + }) + + t.Run("should correctly delete a member for a valid board", func(t *testing.T) { + //nolint:dupl + t.Run("admin removing a user", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newUser2Member := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeEditor: true, + } + user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) + require.NoError(t, err) + require.NotNil(t, user2Member) + require.False(t, user2Member.SchemeAdmin) + require.True(t, user2Member.SchemeEditor) + + memberToDelete := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + } + + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 2) + + success, resp := th.Client.DeleteBoardMember(memberToDelete) + th.CheckOK(resp) + require.True(t, success) + + members, err = th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + }) + + //nolint:dupl + t.Run("user removing themselves", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newUser2Member := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeEditor: true, + } + user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) + require.NoError(t, err) + require.NotNil(t, user2Member) + require.False(t, user2Member.SchemeAdmin) + require.True(t, user2Member.SchemeEditor) + + memberToDelete := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + } + + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 2) + + success, resp := th.Client2.DeleteBoardMember(memberToDelete) + th.CheckOK(resp) + require.True(t, success) + + members, err = th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + }) + + //nolint:dupl + t.Run("a non admin user should not be able to remove another user", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + newUser2Member := &model.BoardMember{ + UserID: th.GetUser2().ID, + BoardID: board.ID, + SchemeEditor: true, + } + user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) + require.NoError(t, err) + require.NotNil(t, user2Member) + require.False(t, user2Member.SchemeAdmin) + require.True(t, user2Member.SchemeEditor) + + memberToDelete := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + } + + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 2) + + success, resp := th.Client2.DeleteBoardMember(memberToDelete) + th.CheckForbidden(resp) + require.False(t, success) + + members, err = th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 2) + }) + }) + + t.Run("should not delete a member if that means that a board will not have any admin", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBoard := &model.Board{ + Title: "title", + Type: model.BoardTypePrivate, + TeamID: teamID, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + + memberToDelete := &model.BoardMember{ + UserID: th.GetUser1().ID, + BoardID: board.ID, + } + + success, resp := th.Client.DeleteBoardMember(memberToDelete) + th.CheckBadRequest(resp) + require.False(t, success) + + members, err := th.Server.App().GetMembersForBoard(board.ID) + require.NoError(t, err) + require.Len(t, members, 1) + require.True(t, members[0].SchemeAdmin) + }) +} diff --git a/server/integrationtests/boards_and_blocks_test.go b/server/integrationtests/boards_and_blocks_test.go new file mode 100644 index 000000000..bd23161fd --- /dev/null +++ b/server/integrationtests/boards_and_blocks_test.go @@ -0,0 +1,816 @@ +package integrationtests + +import ( + "testing" + + "github.com/mattermost/focalboard/server/model" + + "github.com/stretchr/testify/require" +) + +func TestCreateBoardsAndBlocks(t *testing.T) { + teamID := testTeamID + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).Start() + defer th.TearDown() + + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{}, + Blocks: []model.Block{}, + } + + bab, resp := th.Client.CreateBoardsAndBlocks(newBab) + th.CheckUnauthorized(resp) + require.Nil(t, bab) + }) + + t.Run("invalid boards and blocks", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + t.Run("no boards", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{}, + Blocks: []model.Block{ + {ID: "block-id", BoardID: "board-id", Type: model.TypeCard}, + }, + } + + bab, resp := th.Client.CreateBoardsAndBlocks(newBab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("no blocks", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, + }, + Blocks: []model.Block{}, + } + + bab, resp := th.Client.CreateBoardsAndBlocks(newBab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("blocks from nonexistent boards", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, + }, + Blocks: []model.Block{ + {ID: "block-id", BoardID: "nonexistent-board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, + }, + } + + bab, resp := th.Client.CreateBoardsAndBlocks(newBab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("boards with no IDs", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, + {TeamID: teamID, Type: model.BoardTypePrivate}, + }, + Blocks: []model.Block{ + {ID: "block-id", BoardID: "board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, + }, + } + + bab, resp := th.Client.CreateBoardsAndBlocks(newBab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("boards from different teams", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-1", TeamID: "team-id-1", Type: model.BoardTypePrivate}, + {ID: "board-id-2", TeamID: "team-id-2", Type: model.BoardTypePrivate}, + }, + Blocks: []model.Block{ + {ID: "block-id", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, + }, + } + + bab, resp := th.Client.CreateBoardsAndBlocks(newBab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("creating boards and blocks", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen}, + {ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate}, + }, + Blocks: []model.Block{ + {ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, + {ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, + }, + } + + bab, resp := th.Client.CreateBoardsAndBlocks(newBab) + th.CheckOK(resp) + require.NotNil(t, bab) + + require.Len(t, bab.Boards, 2) + require.Len(t, bab.Blocks, 2) + + // board 1 should have been created with a new ID, and its + // block should be there too + boardsTermPublic, resp := th.Client.SearchBoardsForTeam(teamID, "public") + th.CheckOK(resp) + require.Len(t, boardsTermPublic, 1) + board1 := boardsTermPublic[0] + require.Equal(t, "public board", board1.Title) + require.Equal(t, model.BoardTypeOpen, board1.Type) + require.NotEqual(t, "board-id-1", board1.ID) + blocks1, err := th.Server.App().GetBlocksForBoard(board1.ID) + require.NoError(t, err) + require.Len(t, blocks1, 1) + require.Equal(t, "block 1", blocks1[0].Title) + + // board 1 should have been created with a new ID, and its + // block should be there too + boardsTermPrivate, resp := th.Client.SearchBoardsForTeam(teamID, "private") + th.CheckOK(resp) + require.Len(t, boardsTermPrivate, 1) + board2 := boardsTermPrivate[0] + require.Equal(t, "private board", board2.Title) + require.Equal(t, model.BoardTypePrivate, board2.Type) + require.NotEqual(t, "board-id-2", board2.ID) + blocks2, err := th.Server.App().GetBlocksForBoard(board2.ID) + require.NoError(t, err) + require.Len(t, blocks2, 1) + require.Equal(t, "block 2", blocks2[0].Title) + + // user should be an admin of both newly created boards + user1 := th.GetUser1() + members1, err := th.Server.App().GetMembersForBoard(board1.ID) + require.NoError(t, err) + require.Len(t, members1, 1) + require.Equal(t, user1.ID, members1[0].UserID) + members2, err := th.Server.App().GetMembersForBoard(board2.ID) + require.NoError(t, err) + require.Len(t, members2, 1) + require.Equal(t, user1.ID, members2[0].UserID) + }) + }) +} + +func TestPatchBoardsAndBlocks(t *testing.T) { + teamID := "team-id" + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).Start() + defer th.TearDown() + + pbab := &model.PatchBoardsAndBlocks{} + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckUnauthorized(resp) + require.Nil(t, bab) + }) + + t.Run("invalid patch boards and blocks", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + userID := th.GetUser1().ID + initialTitle := "initial title 1" + newTitle := "new title 1" + + newBoard1 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) + require.NoError(t, err) + require.NotNil(t, board1) + + newBoard2 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) + require.NoError(t, err) + require.NotNil(t, board2) + + newBlock1 := model.Block{ + ID: "block-id-1", + BoardID: board1.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) + block1, err := th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.NotNil(t, block1) + + newBlock2 := model.Block{ + ID: "block-id-2", + BoardID: board2.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) + block2, err := th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.NotNil(t, block2) + + t.Run("no board IDs", func(t *testing.T) { + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, block2.ID}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("missmatch board IDs and patches", func(t *testing.T) { + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID, board2.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, block2.ID}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("no block IDs", func(t *testing.T) { + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID, board2.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + BlockIDs: []string{}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("missmatch block IDs and patches", func(t *testing.T) { + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID, board2.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, block2.ID}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("block that doesn't belong to any board", func(t *testing.T) { + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, "board-id-2"}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + }) + + t.Run("if the user doesn't have permissions for one of the boards, nothing should be updated", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + userID := th.GetUser1().ID + initialTitle := "initial title 2" + newTitle := "new title 2" + + newBoard1 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) + require.NoError(t, err) + require.NotNil(t, board1) + + newBoard2 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := th.Server.App().CreateBoard(newBoard2, userID, false) + require.NoError(t, err) + require.NotNil(t, board2) + + newBlock1 := model.Block{ + ID: "block-id-1", + BoardID: board1.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) + block1, err := th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.NotNil(t, block1) + + newBlock2 := model.Block{ + ID: "block-id-2", + BoardID: board2.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) + block2, err := th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.NotNil(t, block2) + + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID, board2.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, block2.ID}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckForbidden(resp) + require.Nil(t, bab) + }) + + t.Run("boards belonging to different teams should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + userID := th.GetUser1().ID + initialTitle := "initial title 3" + newTitle := "new title 3" + + newBoard1 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) + require.NoError(t, err) + require.NotNil(t, board1) + + newBoard2 := &model.Board{ + Title: initialTitle, + TeamID: "different-team-id", + Type: model.BoardTypeOpen, + } + board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) + require.NoError(t, err) + require.NotNil(t, board2) + + newBlock1 := model.Block{ + ID: "block-id-1", + BoardID: board1.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) + block1, err := th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.NotNil(t, block1) + + newBlock2 := model.Block{ + ID: "block-id-2", + BoardID: board2.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) + block2, err := th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.NotNil(t, block2) + + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID, board2.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, "board-id-2"}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("patches should be rejected if one is invalid", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + userID := th.GetUser1().ID + initialTitle := "initial title 4" + newTitle := "new title 4" + + newBoard1 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) + require.NoError(t, err) + require.NotNil(t, board1) + + newBoard2 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := th.Server.App().CreateBoard(newBoard2, userID, false) + require.NoError(t, err) + require.NotNil(t, board2) + + newBlock1 := model.Block{ + ID: "block-id-1", + BoardID: board1.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) + block1, err := th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.NotNil(t, block1) + + newBlock2 := model.Block{ + ID: "block-id-2", + BoardID: board2.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) + block2, err := th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.NotNil(t, block2) + + var invalidPatchType model.BoardType = "invalid" + invalidPatch := &model.BoardPatch{Type: &invalidPatchType} + + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID, board2.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + invalidPatch, + }, + BlockIDs: []string{block1.ID, "board-id-2"}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("patches should be rejected if there is a block that doesn't belong to the boards being patched", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + userID := th.GetUser1().ID + initialTitle := "initial title" + newTitle := "new title" + + newBoard1 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) + require.NoError(t, err) + require.NotNil(t, board1) + + newBoard2 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) + require.NoError(t, err) + require.NotNil(t, board2) + + newBlock1 := model.Block{ + ID: "block-id-1", + BoardID: board1.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) + block1, err := th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.NotNil(t, block1) + + newBlock2 := model.Block{ + ID: "block-id-2", + BoardID: board2.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) + block2, err := th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.NotNil(t, block2) + + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, block2.ID}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckBadRequest(resp) + require.Nil(t, bab) + }) + + t.Run("patches should be applied if they're valid and they're related", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + userID := th.GetUser1().ID + initialTitle := "initial title" + newTitle := "new title" + + newBoard1 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) + require.NoError(t, err) + require.NotNil(t, board1) + + newBoard2 := &model.Board{ + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) + require.NoError(t, err) + require.NotNil(t, board2) + + newBlock1 := model.Block{ + ID: "block-id-1", + BoardID: board1.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) + block1, err := th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.NotNil(t, block1) + + newBlock2 := model.Block{ + ID: "block-id-2", + BoardID: board2.ID, + Title: initialTitle, + } + require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) + block2, err := th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.NotNil(t, block2) + + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{board1.ID, board2.ID}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + BlockIDs: []string{block1.ID, block2.ID}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + bab, resp := th.Client.PatchBoardsAndBlocks(pbab) + th.CheckOK(resp) + require.NotNil(t, bab) + require.Len(t, bab.Boards, 2) + require.Len(t, bab.Blocks, 2) + + // ensure that the entities have been updated + rBoard1, err := th.Server.App().GetBoard(board1.ID) + require.NoError(t, err) + require.Equal(t, newTitle, rBoard1.Title) + rBlock1, err := th.Server.App().GetBlockByID(block1.ID) + require.NoError(t, err) + require.Equal(t, newTitle, rBlock1.Title) + + rBoard2, err := th.Server.App().GetBoard(board2.ID) + require.NoError(t, err) + require.Equal(t, newTitle, rBoard2.Title) + rBlock2, err := th.Server.App().GetBlockByID(block2.ID) + require.NoError(t, err) + require.Equal(t, newTitle, rBlock2.Title) + }) +} + +func TestDeleteBoardsAndBlocks(t *testing.T) { + teamID := "team-id" + + t.Run("a non authenticated user should be rejected", func(t *testing.T) { + th := SetupTestHelper(t).Start() + defer th.TearDown() + + dbab := &model.DeleteBoardsAndBlocks{} + + success, resp := th.Client.DeleteBoardsAndBlocks(dbab) + th.CheckUnauthorized(resp) + require.False(t, success) + }) + + t.Run("invalid delete boards and blocks", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + // a board is required for the permission checks + newBoard := &model.Board{ + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + require.NotNil(t, board) + + t.Run("no boards", func(t *testing.T) { + dbab := &model.DeleteBoardsAndBlocks{ + Blocks: []string{"block-id-1"}, + } + + success, resp := th.Client.DeleteBoardsAndBlocks(dbab) + th.CheckBadRequest(resp) + require.False(t, success) + }) + + t.Run("no blocks", func(t *testing.T) { + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{board.ID}, + } + + success, resp := th.Client.DeleteBoardsAndBlocks(dbab) + th.CheckBadRequest(resp) + require.False(t, success) + }) + + t.Run("boards from different teams", func(t *testing.T) { + newOtherTeamsBoard := &model.Board{ + TeamID: "another-team-id", + Type: model.BoardTypeOpen, + } + otherTeamsBoard, err := th.Server.App().CreateBoard(newOtherTeamsBoard, th.GetUser1().ID, true) + require.NoError(t, err) + require.NotNil(t, board) + + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{board.ID, otherTeamsBoard.ID}, + Blocks: []string{"block-id-1"}, + } + + success, resp := th.Client.DeleteBoardsAndBlocks(dbab) + th.CheckBadRequest(resp) + require.False(t, success) + }) + }) + + t.Run("if the user has no permissions to one of the boards, nothing should be deleted", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + // the user is an admin of the first board + newBoard1 := &model.Board{ + Type: model.BoardTypeOpen, + } + board1, err := th.Server.App().CreateBoard(newBoard1, th.GetUser1().ID, true) + require.NoError(t, err) + require.NotNil(t, board1) + + // but not of the second + newBoard2 := &model.Board{ + Type: model.BoardTypeOpen, + } + board2, err := th.Server.App().CreateBoard(newBoard2, th.GetUser1().ID, false) + require.NoError(t, err) + require.NotNil(t, board2) + + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{board1.ID, board2.ID}, + Blocks: []string{"block-id-1"}, + } + + success, resp := th.Client.DeleteBoardsAndBlocks(dbab) + th.CheckForbidden(resp) + require.False(t, success) + }) + + t.Run("all boards and blocks should be deleted if the request is correct", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen}, + {ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate}, + }, + Blocks: []model.Block{ + {ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, + {ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, + }, + } + + bab, err := th.Server.App().CreateBoardsAndBlocks(newBab, th.GetUser1().ID, true) + require.NoError(t, err) + require.Len(t, bab.Boards, 2) + require.Len(t, bab.Blocks, 2) + + // ensure that the entities have been successfully created + board1, err := th.Server.App().GetBoard("board-id-1") + require.NoError(t, err) + require.NotNil(t, board1) + block1, err := th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.NotNil(t, block1) + + board2, err := th.Server.App().GetBoard("board-id-2") + require.NoError(t, err) + require.NotNil(t, board2) + block2, err := th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.NotNil(t, block2) + + // call the API to delete boards and blocks + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{"board-id-1", "board-id-2"}, + Blocks: []string{"block-id-1", "block-id-2"}, + } + + success, resp := th.Client.DeleteBoardsAndBlocks(dbab) + th.CheckOK(resp) + require.True(t, success) + + // ensure that the entities have been successfully deleted + board1, err = th.Server.App().GetBoard("board-id-1") + require.NoError(t, err) + require.Nil(t, board1) + block1, err = th.Server.App().GetBlockByID("block-id-1") + require.NoError(t, err) + require.Nil(t, block1) + + board2, err = th.Server.App().GetBoard("board-id-2") + require.NoError(t, err) + require.Nil(t, board2) + block2, err = th.Server.App().GetBlockByID("block-id-2") + require.NoError(t, err) + require.Nil(t, block2) + }) +} diff --git a/server/integrationtests/clienttestlib.go b/server/integrationtests/clienttestlib.go index dbceb4afb..b46c3b045 100644 --- a/server/integrationtests/clienttestlib.go +++ b/server/integrationtests/clienttestlib.go @@ -4,19 +4,30 @@ import ( "errors" "net/http" "os" + "testing" "time" "github.com/mattermost/focalboard/server/api" "github.com/mattermost/focalboard/server/client" + "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/server" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/permissions/localpermissions" "github.com/mattermost/focalboard/server/services/store/sqlstore" - "github.com/mattermost/focalboard/server/utils" "github.com/mattermost/mattermost-server/v6/shared/mlog" + + "github.com/stretchr/testify/require" +) + +const ( + user1Username = "user1" + user2Username = "user2" + password = "Pa$$word" ) type TestHelper struct { + T *testing.T Server *server.Server Client *client.Client Client2 *client.Client @@ -80,11 +91,14 @@ func newTestServer(singleUserToken string) *server.Server { panic(err) } + permissionsService := localpermissions.New(db, logger) + params := server.Params{ - Cfg: cfg, - SingleUserToken: singleUserToken, - DBStore: db, - Logger: logger, + Cfg: cfg, + SingleUserToken: singleUserToken, + DBStore: db, + Logger: logger, + PermissionsService: permissionsService, } srv, err := server.New(params) @@ -95,23 +109,26 @@ func newTestServer(singleUserToken string) *server.Server { return srv } -func SetupTestHelper() *TestHelper { +func SetupTestHelperWithToken(t *testing.T) *TestHelper { sessionToken := "TESTTOKEN" - th := &TestHelper{} + th := &TestHelper{T: t} th.Server = newTestServer(sessionToken) th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken) + th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken) return th } -func SetupTestHelperWithoutToken() *TestHelper { - th := &TestHelper{} +func SetupTestHelper(t *testing.T) *TestHelper { + th := &TestHelper{T: t} th.Server = newTestServer("") th.Client = client.NewClient(th.Server.Config().ServerRoot, "") th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "") return th } -func (th *TestHelper) InitBasic() *TestHelper { +// Start starts the test server and ensures that it's correctly +// responding to requests before returning. +func (th *TestHelper) Start() *TestHelper { go func() { if err := th.Server.Start(); err != nil { panic(err) @@ -144,51 +161,28 @@ func (th *TestHelper) InitBasic() *TestHelper { return th } -var ErrRegisterFail = errors.New("register failed") +// InitBasic starts the test server and initializes the clients of the +// helper, registering them and logging them into the system. +func (th *TestHelper) InitBasic() *TestHelper { + th.Start() -func (th *TestHelper) InitUsers(username1 string, username2 string) error { - workspace, err := th.Server.App().GetRootWorkspace() - if err != nil { - return err - } + // user1 + th.RegisterAndLogin(th.Client, user1Username, "user1@sample.com", password, "") - clients := []*client.Client{th.Client, th.Client2} - usernames := []string{username1, username2} + // get token + team, resp := th.Client.GetTeam("0") + th.CheckOK(resp) + require.NotNil(th.T, team) + require.NotNil(th.T, team.SignupToken) - for i, client := range clients { - // register a new user - password := utils.NewID(utils.IDTypeNone) - registerRequest := &api.RegisterRequest{ - Username: usernames[i], - Email: usernames[i] + "@example.com", - Password: password, - Token: workspace.SignupToken, - } - success, resp := client.Register(registerRequest) - if resp.Error != nil { - return resp.Error - } - if !success { - return ErrRegisterFail - } + // user2 + th.RegisterAndLogin(th.Client2, user2Username, "user2@sample.com", password, team.SignupToken) - // login - loginRequest := &api.LoginRequest{ - Type: "normal", - Username: registerRequest.Username, - Email: registerRequest.Email, - Password: registerRequest.Password, - } - data, resp := client.Login(loginRequest) - if resp.Error != nil { - return resp.Error - } - - client.Token = data.Token - } - return nil + return th } +var ErrRegisterFail = errors.New("register failed") + func (th *TestHelper) TearDown() { defer func() { _ = th.Server.Logger().Shutdown() }() @@ -198,4 +192,96 @@ func (th *TestHelper) TearDown() { } os.RemoveAll(th.Server.Config().FilesPath) + + if err := os.Remove(th.Server.Config().DBConfigString); err == nil { + th.Server.Logger().Debug("Removed test database", mlog.String("file", th.Server.Config().DBConfigString)) + } +} + +func (th *TestHelper) RegisterAndLogin(client *client.Client, username, email, password, token string) { + req := &api.RegisterRequest{ + Username: username, + Email: email, + Password: password, + Token: token, + } + + success, resp := th.Client.Register(req) + th.CheckOK(resp) + require.True(th.T, success) + + th.Login(client, username, password) +} + +func (th *TestHelper) Login(client *client.Client, username, password string) { + req := &api.LoginRequest{ + Type: "normal", + Username: username, + Password: password, + } + data, resp := client.Login(req) + th.CheckOK(resp) + require.NotNil(th.T, data) +} + +func (th *TestHelper) Login1() { + th.Login(th.Client, user1Username, password) +} + +func (th *TestHelper) Login2() { + th.Login(th.Client2, user2Username, password) +} + +func (th *TestHelper) Logout(client *client.Client) { + client.Token = "" +} + +func (th *TestHelper) Me(client *client.Client) *model.User { + user, resp := client.GetMe() + th.CheckOK(resp) + require.NotNil(th.T, user) + return user +} + +func (th *TestHelper) CreateBoard(teamID string, boardType model.BoardType) *model.Board { + newBoard := &model.Board{ + TeamID: teamID, + Type: boardType, + } + board, resp := th.Client.CreateBoard(newBoard) + th.CheckOK(resp) + return board +} + +func (th *TestHelper) GetUser1() *model.User { + return th.Me(th.Client) +} + +func (th *TestHelper) GetUser2() *model.User { + return th.Me(th.Client2) +} + +func (th *TestHelper) CheckOK(r *client.Response) { + require.Equal(th.T, http.StatusOK, r.StatusCode) + require.NoError(th.T, r.Error) +} + +func (th *TestHelper) CheckBadRequest(r *client.Response) { + require.Equal(th.T, http.StatusBadRequest, r.StatusCode) + require.Error(th.T, r.Error) +} + +func (th *TestHelper) CheckNotFound(r *client.Response) { + require.Equal(th.T, http.StatusNotFound, r.StatusCode) + require.Error(th.T, r.Error) +} + +func (th *TestHelper) CheckUnauthorized(r *client.Response) { + require.Equal(th.T, http.StatusUnauthorized, r.StatusCode) + require.Error(th.T, r.Error) +} + +func (th *TestHelper) CheckForbidden(r *client.Response) { + require.Equal(th.T, http.StatusForbidden, r.StatusCode) + require.Error(th.T, r.Error) } diff --git a/server/integrationtests/sharing_test.go b/server/integrationtests/sharing_test.go index 2b2868217..b7724fcbb 100644 --- a/server/integrationtests/sharing_test.go +++ b/server/integrationtests/sharing_test.go @@ -1,6 +1,7 @@ package integrationtests import ( + "net/http" "testing" "github.com/mattermost/focalboard/server/model" @@ -9,60 +10,84 @@ import ( ) func TestSharing(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelper(t).InitBasic() defer th.TearDown() - rootID := utils.NewID(utils.IDTypeBlock) + var boardID string token := utils.NewID(utils.IDTypeToken) + t.Run("an unauthenticated client should not be able to get a sharing", func(t *testing.T) { + th.Logout(th.Client) + + sharing, resp := th.Client.GetSharing("board-id") + th.CheckUnauthorized(resp) + require.Nil(t, sharing) + }) + t.Run("Check no initial sharing", func(t *testing.T) { - sharing, resp := th.Client.GetSharing(rootID) - require.NoError(t, resp.Error) - require.Empty(t, sharing.ID) - require.False(t, sharing.Enabled) + th.Login1() + + teamID := "0" + newBoard := &model.Board{ + TeamID: teamID, + Type: model.BoardTypeOpen, + } + + board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) + require.NoError(t, err) + require.NotNil(t, board) + boardID = board.ID + + s, err := th.Server.App().GetSharing(boardID) + require.NoError(t, err) + require.Nil(t, s) + + sharing, resp := th.Client.GetSharing(boardID) + th.CheckNotFound(resp) + require.Nil(t, sharing) }) t.Run("POST sharing, config = false", func(t *testing.T) { sharing := model.Sharing{ - ID: rootID, + ID: boardID, Token: token, Enabled: true, UpdateAt: 1, } // it will fail with default config - success, resp := th.Client.PostSharing(sharing) + success, resp := th.Client.PostSharing(&sharing) require.False(t, success) require.Error(t, resp.Error) t.Run("GET sharing", func(t *testing.T) { - sharing, resp := th.Client.GetSharing(rootID) - // Expect no error, but no Id returned - require.NoError(t, resp.Error) - require.NotNil(t, sharing) - require.Equal(t, "", sharing.ID) + sharing, resp := th.Client.GetSharing(boardID) + // Expect not found error + require.Error(t, resp.Error) + require.Equal(t, resp.StatusCode, http.StatusNotFound) + require.Nil(t, sharing) }) }) t.Run("POST sharing, config = true", func(t *testing.T) { th.Server.Config().EnablePublicSharedBoards = true sharing := model.Sharing{ - ID: rootID, + ID: boardID, Token: token, Enabled: true, UpdateAt: 1, } // it will succeed with updated config - success, resp := th.Client.PostSharing(sharing) + success, resp := th.Client.PostSharing(&sharing) require.True(t, success) require.NoError(t, resp.Error) t.Run("GET sharing", func(t *testing.T) { - sharing, resp := th.Client.GetSharing(rootID) + sharing, resp := th.Client.GetSharing(boardID) require.NoError(t, resp.Error) require.NotNil(t, sharing) - require.Equal(t, sharing.ID, rootID) + require.Equal(t, sharing.ID, boardID) require.True(t, sharing.Enabled) require.Equal(t, sharing.Token, token) }) diff --git a/server/integrationtests/subscriptions_test.go b/server/integrationtests/subscriptions_test.go index 5b5c6b9de..0a3ab2943 100644 --- a/server/integrationtests/subscriptions_test.go +++ b/server/integrationtests/subscriptions_test.go @@ -6,13 +6,12 @@ import ( "github.com/mattermost/focalboard/server/client" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func createTestSubscriptions(client *client.Client, num int, workspaceID string) ([]*model.Subscription, string, error) { +func createTestSubscriptions(client *client.Client, num int) ([]*model.Subscription, string, error) { newSubs := make([]*model.Subscription, 0, num) user, resp := client.GetMe() @@ -20,29 +19,27 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string) return nil, "", fmt.Errorf("cannot get current user: %w", resp.Error) } - board := model.Block{ - ID: utils.NewID(utils.IDTypeBoard), - RootID: workspaceID, + board := &model.Board{ + TeamID: "0", + Type: model.BoardTypeOpen, CreateAt: 1, UpdateAt: 1, - Type: model.TypeBoard, } - boards, resp := client.InsertBlocks([]model.Block{board}) + board, resp = client.CreateBoard(board) if resp.Error != nil { return nil, "", fmt.Errorf("cannot insert test board block: %w", resp.Error) } - board = boards[0] for n := 0; n < num; n++ { newBlock := model.Block{ ID: utils.NewID(utils.IDTypeCard), - RootID: board.ID, + BoardID: board.ID, CreateAt: 1, UpdateAt: 1, Type: model.TypeCard, } - newBlocks, resp := client.InsertBlocks([]model.Block{newBlock}) + newBlocks, resp := client.InsertBlocks(board.ID, []model.Block{newBlock}) if resp.Error != nil { return nil, "", fmt.Errorf("cannot insert test card block: %w", resp.Error) } @@ -51,12 +48,11 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string) sub := &model.Subscription{ BlockType: newBlock.Type, BlockID: newBlock.ID, - WorkspaceID: workspaceID, SubscriberType: model.SubTypeUser, SubscriberID: user.ID, } - subNew, resp := client.CreateSubscription(workspaceID, sub) + subNew, resp := client.CreateSubscription(sub) if resp.Error != nil { return nil, "", resp.Error } @@ -66,20 +62,16 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string) } func TestCreateSubscription(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelper(t).InitBasic() defer th.TearDown() - container := store.Container{ - WorkspaceID: utils.NewID(utils.IDTypeWorkspace), - } - t.Run("Create valid subscription", func(t *testing.T) { - subs, userID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID) + subs, userID, err := createTestSubscriptions(th.Client, 5) require.NoError(t, err) require.Len(t, subs, 5) // fetch the newly created subscriptions and compare - subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID) + subsFound, resp := th.Client.GetSubscriptions(userID) require.NoError(t, resp.Error) require.Len(t, subsFound, 5) assert.ElementsMatch(t, subs, subsFound) @@ -90,47 +82,38 @@ func TestCreateSubscription(t *testing.T) { require.NoError(t, resp.Error) sub := &model.Subscription{ - WorkspaceID: container.WorkspaceID, SubscriberID: user.ID, } - _, resp = th.Client.CreateSubscription(container.WorkspaceID, sub) + _, resp = th.Client.CreateSubscription(sub) require.Error(t, resp.Error) }) t.Run("Create subscription for another user", func(t *testing.T) { sub := &model.Subscription{ - WorkspaceID: container.WorkspaceID, SubscriberID: utils.NewID(utils.IDTypeUser), } - _, resp := th.Client.CreateSubscription(container.WorkspaceID, sub) + _, resp := th.Client.CreateSubscription(sub) require.Error(t, resp.Error) }) } func TestGetSubscriptions(t *testing.T) { - th := SetupTestHelperWithoutToken().InitBasic() + th := SetupTestHelper(t).InitBasic() defer th.TearDown() - err := th.InitUsers("user1", "user2") - require.NoError(t, err, "failed to init users") - - container := store.Container{ - WorkspaceID: utils.NewID(utils.IDTypeWorkspace), - } - t.Run("Get subscriptions for user", func(t *testing.T) { - mySubs, user1ID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID) + mySubs, user1ID, err := createTestSubscriptions(th.Client, 5) require.NoError(t, err) require.Len(t, mySubs, 5) // create more subscriptions with different user - otherSubs, _, err := createTestSubscriptions(th.Client2, 10, container.WorkspaceID) + otherSubs, _, err := createTestSubscriptions(th.Client2, 10) require.NoError(t, err) require.Len(t, otherSubs, 10) // fetch the newly created subscriptions for current user, making sure only // the ones created for the current user are returned. - subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, user1ID) + subsFound, resp := th.Client.GetSubscriptions(user1ID) require.NoError(t, resp.Error) require.Len(t, subsFound, 5) assert.ElementsMatch(t, mySubs, subsFound) @@ -138,23 +121,19 @@ func TestGetSubscriptions(t *testing.T) { } func TestDeleteSubscription(t *testing.T) { - th := SetupTestHelper().InitBasic() + th := SetupTestHelper(t).InitBasic() defer th.TearDown() - container := store.Container{ - WorkspaceID: utils.NewID(utils.IDTypeWorkspace), - } - t.Run("Delete valid subscription", func(t *testing.T) { - subs, userID, err := createTestSubscriptions(th.Client, 3, container.WorkspaceID) + subs, userID, err := createTestSubscriptions(th.Client, 3) require.NoError(t, err) require.Len(t, subs, 3) - resp := th.Client.DeleteSubscription(container.WorkspaceID, subs[1].BlockID, userID) + resp := th.Client.DeleteSubscription(subs[1].BlockID, userID) require.NoError(t, resp.Error) // fetch the subscriptions and ensure the list is correct - subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID) + subsFound, resp := th.Client.GetSubscriptions(userID) require.NoError(t, resp.Error) require.Len(t, subsFound, 2) @@ -167,7 +146,7 @@ func TestDeleteSubscription(t *testing.T) { user, resp := th.Client.GetMe() require.NoError(t, resp.Error) - resp = th.Client.DeleteSubscription(container.WorkspaceID, "bogus", user.ID) + resp = th.Client.DeleteSubscription("bogus", user.ID) require.Error(t, resp.Error) }) } diff --git a/server/integrationtests/user_test.go b/server/integrationtests/user_test.go index db81ff2da..10b76f235 100644 --- a/server/integrationtests/user_test.go +++ b/server/integrationtests/user_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mattermost/focalboard/server/api" + "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/utils" "github.com/stretchr/testify/require" ) @@ -16,7 +17,7 @@ const ( ) func TestUserRegister(t *testing.T) { - th := SetupTestHelperWithoutToken().InitBasic() + th := SetupTestHelper(t).Start() defer th.TearDown() // register @@ -29,14 +30,14 @@ func TestUserRegister(t *testing.T) { require.NoError(t, resp.Error) require.True(t, success) - // register again will failed + // register again will fail success, resp = th.Client.Register(registerRequest) require.Error(t, resp.Error) require.False(t, success) } func TestUserLogin(t *testing.T) { - th := SetupTestHelperWithoutToken().InitBasic() + th := SetupTestHelper(t).Start() defer th.TearDown() t.Run("with nonexist user", func(t *testing.T) { @@ -78,7 +79,7 @@ func TestUserLogin(t *testing.T) { } func TestGetMe(t *testing.T) { - th := SetupTestHelperWithoutToken().InitBasic() + th := SetupTestHelper(t).Start() defer th.TearDown() t.Run("not login yet", func(t *testing.T) { @@ -120,7 +121,7 @@ func TestGetMe(t *testing.T) { } func TestGetUser(t *testing.T) { - th := SetupTestHelperWithoutToken().InitBasic() + th := SetupTestHelper(t).Start() defer th.TearDown() // register @@ -165,7 +166,7 @@ func TestGetUser(t *testing.T) { } func TestUserChangePassword(t *testing.T) { - th := SetupTestHelperWithoutToken().InitBasic() + th := SetupTestHelper(t).Start() defer th.TearDown() // register @@ -210,30 +211,58 @@ func randomBytes(t *testing.T, n int) []byte { return bb } -func TestWorkspaceUploadFile(t *testing.T) { +func TestTeamUploadFile(t *testing.T) { t.Run("no permission", func(t *testing.T) { // native auth, but not login - th := SetupTestHelperWithoutToken().InitBasic() + th := SetupTestHelper(t).InitBasic() defer th.TearDown() - workspaceID := "0" - rootID := utils.NewID(utils.IDTypeBlock) + teamID := "0" + boardID := utils.NewID(utils.IDTypeBoard) data := randomBytes(t, 1024) - result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data)) + result, resp := th.Client.TeamUploadFile(teamID, boardID, bytes.NewReader(data)) require.Error(t, resp.Error) require.Nil(t, result) }) - t.Run("success", func(t *testing.T) { // single token auth - th := SetupTestHelper().InitBasic() + t.Run("a board admin should be able to update a file", func(t *testing.T) { // single token auth + th := SetupTestHelper(t).InitBasic() defer th.TearDown() - workspaceID := "0" - rootID := utils.NewID(utils.IDTypeBlock) + teamID := "0" + newBoard := &model.Board{ + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, resp := th.Client.CreateBoard(newBoard) + th.CheckOK(resp) + require.NotNil(t, board) + data := randomBytes(t, 1024) - result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data)) - require.NoError(t, resp.Error) + result, resp := th.Client.TeamUploadFile(teamID, board.ID, bytes.NewReader(data)) + th.CheckOK(resp) require.NotNil(t, result) require.NotEmpty(t, result.FileID) // TODO get the uploaded file }) + + t.Run("user that doesn't belong to the board should not be able to upload a file", func(t *testing.T) { + th := SetupTestHelper(t).InitBasic() + defer th.TearDown() + + teamID := "0" + newBoard := &model.Board{ + Type: model.BoardTypeOpen, + TeamID: teamID, + } + board, resp := th.Client.CreateBoard(newBoard) + th.CheckOK(resp) + require.NotNil(t, board) + + data := randomBytes(t, 1024) + + // a user that doesn't belong to the board tries to upload the file + result, resp := th.Client2.TeamUploadFile(teamID, board.ID, bytes.NewReader(data)) + th.CheckForbidden(resp) + require.Nil(t, result) + }) } diff --git a/server/main/main.go b/server/main/main.go index adaec9b0b..9d13531d7 100644 --- a/server/main/main.go +++ b/server/main/main.go @@ -13,6 +13,7 @@ import ( "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/server" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/permissions/localpermissions" ) import ( "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -145,11 +146,14 @@ func main() { logger.Fatal("server.NewStore ERROR", mlog.Err(err)) } + permissionsService := localpermissions.New(db, logger) + params := server.Params{ - Cfg: config, - SingleUserToken: singleUserToken, - DBStore: db, - Logger: logger, + Cfg: config, + SingleUserToken: singleUserToken, + DBStore: db, + Logger: logger, + PermissionsService: permissionsService, } server, err := server.New(params) @@ -233,11 +237,14 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db logger.Fatal("server.NewStore ERROR", mlog.Err(err)) } + permissionsService := localpermissions.New(db, logger) + params := server.Params{ - Cfg: config, - SingleUserToken: singleUserToken, - DBStore: db, - Logger: logger, + Cfg: config, + SingleUserToken: singleUserToken, + DBStore: db, + Logger: logger, + PermissionsService: permissionsService, } pServer, err = server.New(params) diff --git a/server/model/block.go b/server/model/block.go index d024db6fc..95c1ca8f2 100644 --- a/server/model/block.go +++ b/server/model/block.go @@ -19,10 +19,6 @@ type Block struct { // required: false ParentID string `json:"parentId"` - // The id for this block's root block - // required: true - RootID string `json:"rootId"` - // The id for user who created this block // required: true CreatedBy string `json:"createdBy"` @@ -59,9 +55,13 @@ type Block struct { // required: false DeleteAt int64 `json:"deleteAt"` - // The workspace id that the block belongs to + // Deprecated. The workspace id that the block belongs to + // required: false + WorkspaceID string `json:"-"` + + // The board id that the block belongs to // required: true - WorkspaceID string `json:"workspaceId"` + BoardID string `json:"boardId"` } // BlockPatch is a patch for modify blocks @@ -71,10 +71,6 @@ type BlockPatch struct { // required: false ParentID *string `json:"parentId"` - // The id for this block's root block - // required: false - RootID *string `json:"rootId"` - // The schema version of this block // required: false Schema *int64 `json:"schema"` @@ -94,6 +90,10 @@ type BlockPatch struct { // The block removed fields // required: false DeletedFields []string `json:"deletedFields"` + + // The board id that the block belongs to + // required: false + BoardID *string `json:"boardId"` } // BlockPatchBatch is a batch of IDs and patches for modify blocks @@ -106,11 +106,11 @@ type BlockPatchBatch struct { BlockPatches []BlockPatch `json:"block_patches"` } -// BlockModifier is a callback that can modify each block during an import. +// BoardModifier is a callback that can modify each board during an import. // A cache of arbitrary data will be passed for each call and any changes // to the cache will be preserved for the next call. // Return true to import the block or false to skip import. -type BlockModifier func(block *Block, cache map[string]interface{}) bool +type BoardModifier func(board *Board, cache map[string]interface{}) bool func BlocksFromJSON(data io.Reader) []Block { var blocks []Block @@ -123,12 +123,12 @@ func (b Block) LogClone() interface{} { return struct { ID string ParentID string - RootID string + BoardID string Type BlockType }{ ID: b.ID, ParentID: b.ParentID, - RootID: b.RootID, + BoardID: b.BoardID, Type: b.Type, } } @@ -139,8 +139,8 @@ func (p *BlockPatch) Patch(block *Block) *Block { block.ParentID = *p.ParentID } - if p.RootID != nil { - block.RootID = *p.RootID + if p.BoardID != nil { + block.BoardID = *p.BoardID } if p.Schema != nil { diff --git a/server/model/block_test.go b/server/model/block_test.go index 2ade17f0d..32c563688 100644 --- a/server/model/block_test.go +++ b/server/model/block_test.go @@ -20,61 +20,61 @@ func TestGenerateBlockIDs(t *testing.T) { blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) require.NotEqual(t, blockID, blocks[0].ID) - require.Zero(t, blocks[0].RootID) + require.Zero(t, blocks[0].BoardID) require.Zero(t, blocks[0].ParentID) }) t.Run("Should generate a new ID for a single block with references", func(t *testing.T) { blockID := utils.NewID(utils.IDTypeBlock) - rootID := utils.NewID(utils.IDTypeBlock) + boardID := utils.NewID(utils.IDTypeBlock) parentID := utils.NewID(utils.IDTypeBlock) - blocks := []Block{{ID: blockID, RootID: rootID, ParentID: parentID}} + blocks := []Block{{ID: blockID, BoardID: boardID, ParentID: parentID}} blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) require.NotEqual(t, blockID, blocks[0].ID) - require.Equal(t, rootID, blocks[0].RootID) + require.Equal(t, boardID, blocks[0].BoardID) require.Equal(t, parentID, blocks[0].ParentID) }) t.Run("Should generate IDs and link multiple blocks with existing references", func(t *testing.T) { blockID1 := utils.NewID(utils.IDTypeBlock) - rootID1 := utils.NewID(utils.IDTypeBlock) + boardID1 := utils.NewID(utils.IDTypeBlock) parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1} + block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} blockID2 := utils.NewID(utils.IDTypeBlock) - rootID2 := blockID1 + boardID2 := blockID1 parentID2 := utils.NewID(utils.IDTypeBlock) - block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2} + block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} blocks := []Block{block1, block2} blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) require.NotEqual(t, blockID1, blocks[0].ID) - require.Equal(t, rootID1, blocks[0].RootID) + require.Equal(t, boardID1, blocks[0].BoardID) require.Equal(t, parentID1, blocks[0].ParentID) require.NotEqual(t, blockID2, blocks[1].ID) - require.NotEqual(t, rootID2, blocks[1].RootID) + require.NotEqual(t, boardID2, blocks[1].BoardID) require.Equal(t, parentID2, blocks[1].ParentID) // blockID1 was referenced, so it should still be after the ID // changes - require.Equal(t, blocks[0].ID, blocks[1].RootID) + require.Equal(t, blocks[0].ID, blocks[1].BoardID) }) t.Run("Should generate new IDs but not modify nonexisting references", func(t *testing.T) { blockID1 := utils.NewID(utils.IDTypeBlock) - rootID1 := "" + boardID1 := "" parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1} + block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} blockID2 := utils.NewID(utils.IDTypeBlock) - rootID2 := utils.NewID(utils.IDTypeBlock) + boardID2 := utils.NewID(utils.IDTypeBlock) parentID2 := "" - block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2} + block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} blocks := []Block{block1, block2} @@ -82,37 +82,37 @@ func TestGenerateBlockIDs(t *testing.T) { // only the IDs should have changed require.NotEqual(t, blockID1, blocks[0].ID) - require.Zero(t, blocks[0].RootID) + require.Zero(t, blocks[0].BoardID) require.Equal(t, parentID1, blocks[0].ParentID) require.NotEqual(t, blockID2, blocks[1].ID) - require.Equal(t, rootID2, blocks[1].RootID) + require.Equal(t, boardID2, blocks[1].BoardID) require.Zero(t, blocks[1].ParentID) }) t.Run("Should modify correctly multiple blocks with existing and nonexisting references", func(t *testing.T) { blockID1 := utils.NewID(utils.IDTypeBlock) - rootID1 := utils.NewID(utils.IDTypeBlock) + boardID1 := utils.NewID(utils.IDTypeBlock) parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1} + block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} // linked to 1 blockID2 := utils.NewID(utils.IDTypeBlock) - rootID2 := blockID1 + boardID2 := blockID1 parentID2 := utils.NewID(utils.IDTypeBlock) - block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2} + block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} // linked to 2 blockID3 := utils.NewID(utils.IDTypeBlock) - rootID3 := blockID2 + boardID3 := blockID2 parentID3 := utils.NewID(utils.IDTypeBlock) - block3 := Block{ID: blockID3, RootID: rootID3, ParentID: parentID3} + block3 := Block{ID: blockID3, BoardID: boardID3, ParentID: parentID3} // linked to 1 blockID4 := utils.NewID(utils.IDTypeBlock) - rootID4 := blockID1 + boardID4 := blockID1 parentID4 := utils.NewID(utils.IDTypeBlock) - block4 := Block{ID: blockID4, RootID: rootID4, ParentID: parentID4} + block4 := Block{ID: blockID4, BoardID: boardID4, ParentID: parentID4} // blocks are shuffled blocks := []Block{block4, block2, block1, block3} @@ -121,44 +121,44 @@ func TestGenerateBlockIDs(t *testing.T) { // block 1 require.NotEqual(t, blockID1, blocks[2].ID) - require.Equal(t, rootID1, blocks[2].RootID) + require.Equal(t, boardID1, blocks[2].BoardID) require.Equal(t, parentID1, blocks[2].ParentID) // block 2 require.NotEqual(t, blockID2, blocks[1].ID) - require.NotEqual(t, rootID2, blocks[1].RootID) - require.Equal(t, blocks[2].ID, blocks[1].RootID) // link to 1 + require.NotEqual(t, boardID2, blocks[1].BoardID) + require.Equal(t, blocks[2].ID, blocks[1].BoardID) // link to 1 require.Equal(t, parentID2, blocks[1].ParentID) // block 3 require.NotEqual(t, blockID3, blocks[3].ID) - require.NotEqual(t, rootID3, blocks[3].RootID) - require.Equal(t, blocks[1].ID, blocks[3].RootID) // link to 2 + require.NotEqual(t, boardID3, blocks[3].BoardID) + require.Equal(t, blocks[1].ID, blocks[3].BoardID) // link to 2 require.Equal(t, parentID3, blocks[3].ParentID) // block 4 require.NotEqual(t, blockID4, blocks[0].ID) - require.NotEqual(t, rootID4, blocks[0].RootID) - require.Equal(t, blocks[2].ID, blocks[0].RootID) // link to 1 + require.NotEqual(t, boardID4, blocks[0].BoardID) + require.Equal(t, blocks[2].ID, blocks[0].BoardID) // link to 1 require.Equal(t, parentID4, blocks[0].ParentID) }) t.Run("Should update content order", func(t *testing.T) { blockID1 := utils.NewID(utils.IDTypeBlock) - rootID1 := utils.NewID(utils.IDTypeBlock) + boardID1 := utils.NewID(utils.IDTypeBlock) parentID1 := utils.NewID(utils.IDTypeBlock) block1 := Block{ ID: blockID1, - RootID: rootID1, + BoardID: boardID1, ParentID: parentID1, } blockID2 := utils.NewID(utils.IDTypeBlock) - rootID2 := utils.NewID(utils.IDTypeBlock) + boardID2 := utils.NewID(utils.IDTypeBlock) parentID2 := utils.NewID(utils.IDTypeBlock) block2 := Block{ ID: blockID2, - RootID: rootID2, + BoardID: boardID2, ParentID: parentID2, Fields: map[string]interface{}{ "contentOrder": []interface{}{ @@ -172,11 +172,11 @@ func TestGenerateBlockIDs(t *testing.T) { blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) require.NotEqual(t, blockID1, blocks[0].ID) - require.Equal(t, rootID1, blocks[0].RootID) + require.Equal(t, boardID1, blocks[0].BoardID) require.Equal(t, parentID1, blocks[0].ParentID) require.NotEqual(t, blockID2, blocks[1].ID) - require.Equal(t, rootID2, blocks[1].RootID) + require.Equal(t, boardID2, blocks[1].BoardID) require.Equal(t, parentID2, blocks[1].ParentID) // since block 1 was referenced in block 2, @@ -189,35 +189,35 @@ func TestGenerateBlockIDs(t *testing.T) { t.Run("Should update content order when it contain slices", func(t *testing.T) { blockID1 := utils.NewID(utils.IDTypeBlock) - rootID1 := utils.NewID(utils.IDTypeBlock) + boardID1 := utils.NewID(utils.IDTypeBlock) parentID1 := utils.NewID(utils.IDTypeBlock) block1 := Block{ ID: blockID1, - RootID: rootID1, + BoardID: boardID1, ParentID: parentID1, } blockID2 := utils.NewID(utils.IDTypeBlock) block2 := Block{ ID: blockID2, - RootID: rootID1, + BoardID: boardID1, ParentID: parentID1, } blockID3 := utils.NewID(utils.IDTypeBlock) block3 := Block{ ID: blockID3, - RootID: rootID1, + BoardID: boardID1, ParentID: parentID1, } blockID4 := utils.NewID(utils.IDTypeBlock) - rootID2 := utils.NewID(utils.IDTypeBlock) + boardID2 := utils.NewID(utils.IDTypeBlock) parentID2 := utils.NewID(utils.IDTypeBlock) block4 := Block{ ID: blockID4, - RootID: rootID2, + BoardID: boardID2, ParentID: parentID2, Fields: map[string]interface{}{ "contentOrder": []interface{}{ @@ -235,11 +235,11 @@ func TestGenerateBlockIDs(t *testing.T) { blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) require.NotEqual(t, blockID1, blocks[0].ID) - require.Equal(t, rootID1, blocks[0].RootID) + require.Equal(t, boardID1, blocks[0].BoardID) require.Equal(t, parentID1, blocks[0].ParentID) require.NotEqual(t, blockID4, blocks[3].ID) - require.Equal(t, rootID2, blocks[3].RootID) + require.Equal(t, boardID2, blocks[3].BoardID) require.Equal(t, parentID2, blocks[3].ParentID) // since block 1 was referenced in block 2, diff --git a/server/model/blockid.go b/server/model/blockid.go index d7b92c116..4018e40b8 100644 --- a/server/model/blockid.go +++ b/server/model/blockid.go @@ -20,8 +20,8 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block { blockIDs[block.ID] = block.Type } - if _, ok := referenceIDs[block.RootID]; !ok { - referenceIDs[block.RootID] = true + if _, ok := referenceIDs[block.BoardID]; !ok { + referenceIDs[block.BoardID] = true } if _, ok := referenceIDs[block.ParentID]; !ok { referenceIDs[block.ParentID] = true @@ -81,7 +81,7 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block { newBlocks := make([]Block, len(blocks)) for i, block := range blocks { block.ID = getExistingOrNewID(block.ID) - block.RootID = getExistingOrOldID(block.RootID) + block.BoardID = getExistingOrOldID(block.BoardID) block.ParentID = getExistingOrOldID(block.ParentID) blockMod := block diff --git a/server/model/board.go b/server/model/board.go new file mode 100644 index 000000000..9b519807a --- /dev/null +++ b/server/model/board.go @@ -0,0 +1,304 @@ +package model + +import ( + "encoding/json" + "io" +) + +type BoardType string + +const ( + BoardTypeOpen BoardType = "O" + BoardTypePrivate BoardType = "P" +) + +// Board groups a set of blocks and its layout +// swagger:model +type Board struct { + // The ID for the board + // required: true + ID string `json:"id"` + + // The ID of the team that the board belongs to + // required: true + TeamID string `json:"teamId"` + + // The ID of the channel that the board was created from + // required: false + ChannelID string `json:"channelId"` + + // The ID of the user that created the board + // required: true + CreatedBy string `json:"createdBy"` + + // The ID of the last user that updated the board + // required: true + ModifiedBy string `json:"modifiedBy"` + + // The type of the board + // required: true + Type BoardType `json:"type"` + + // The title of the board + // required: false + Title string `json:"title"` + + // The description of the board + // required: false + Description string `json:"description"` + + // The icon of the board + // required: false + Icon string `json:"icon"` + + // Indicates if the board shows the description on the interface + // required: false + ShowDescription bool `json:"showDescription"` + + // Marks the template boards + // required: false + IsTemplate bool `json:"isTemplate"` + + // Marks the template boards + // required: false + TemplateVersion int `json:"templateVersion"` + + // The properties of the board + // required: false + Properties map[string]interface{} `json:"properties"` + + // The properties of the board cards + // required: false + CardProperties []map[string]interface{} `json:"cardProperties"` + + // The calculations on the board's cards + // required: false + ColumnCalculations map[string]interface{} `json:"columnCalculations"` + + // The creation time + // required: true + CreateAt int64 `json:"createAt"` + + // The last modified time + // required: true + UpdateAt int64 `json:"updateAt"` + + // The deleted time. Set to indicate this block is deleted + // required: false + DeleteAt int64 `json:"deleteAt"` +} + +// BoardPatch is a patch for modify boards +// swagger:model +type BoardPatch struct { + // The type of the board + // required: false + Type *BoardType `json:"type"` + + // The title of the board + // required: false + Title *string `json:"title"` + + // The description of the board + // required: false + Description *string `json:"description"` + + // The icon of the board + // required: false + Icon *string `json:"icon"` + + // Indicates if the board shows the description on the interface + // required: false + ShowDescription *bool `json:"showDescription"` + + // The board updated properties + // required: false + UpdatedProperties map[string]interface{} `json:"updatedProperties"` + + // The board removed properties + // required: false + DeletedProperties []string `json:"deletedProperties"` + + // The board updated card properties + // required: false + UpdatedCardProperties []map[string]interface{} `json:"updatedCardProperties"` + + // The board removed card properties + // required: false + DeletedCardProperties []string `json:"deletedCardProperties"` + + // The board updated column calculations + // required: false + UpdatedColumnCalculations map[string]interface{} `json:"updatedColumnCalculations"` + + // The board deleted column calculations + // required: false + DeletedColumnCalculations []string `json:"deletedColumnCalculations"` +} + +// BoardMember stores the information of the membership of a user on a board +// swagger:model +type BoardMember struct { + // The ID of the board + // required: true + BoardID string `json:"boardId"` + + // The ID of the user + // required: true + UserID string `json:"userId"` + + // The independent roles of the user on the board + // required: false + Roles string `json:"roles"` + + // Marks the user as an admin of the board + // required: true + SchemeAdmin bool `json:"schemeAdmin"` + + // Marks the user as an editor of the board + // required: true + SchemeEditor bool `json:"schemeEditor"` + + // Marks the user as an commenter of the board + // required: true + SchemeCommenter bool `json:"schemeCommenter"` + + // Marks the user as an viewer of the board + // required: true + SchemeViewer bool `json:"schemeViewer"` +} + +func BoardFromJSON(data io.Reader) *Board { + var board *Board + _ = json.NewDecoder(data).Decode(&board) + return board +} + +func BoardsFromJSON(data io.Reader) []*Board { + var boards []*Board + _ = json.NewDecoder(data).Decode(&boards) + return boards +} + +func BoardMemberFromJSON(data io.Reader) *BoardMember { + var boardMember *BoardMember + _ = json.NewDecoder(data).Decode(&boardMember) + return boardMember +} + +func BoardMembersFromJSON(data io.Reader) []*BoardMember { + var boardMembers []*BoardMember + _ = json.NewDecoder(data).Decode(&boardMembers) + return boardMembers +} + +// Patch returns an updated version of the board. +func (p *BoardPatch) Patch(board *Board) *Board { + if p.Type != nil { + board.Type = *p.Type + } + + if p.Title != nil { + board.Title = *p.Title + } + + if p.Description != nil { + board.Description = *p.Description + } + + if p.Icon != nil { + board.Icon = *p.Icon + } + + if p.ShowDescription != nil { + board.ShowDescription = *p.ShowDescription + } + + for key, property := range p.UpdatedProperties { + board.Properties[key] = property + } + + for _, key := range p.DeletedProperties { + delete(board.Properties, key) + } + + if len(p.UpdatedCardProperties) != 0 || len(p.DeletedCardProperties) != 0 { + // first we accumulate all properties indexed by ID + cardPropertyMap := map[string]map[string]interface{}{} + for _, prop := range board.CardProperties { + id, ok := prop["id"].(string) + if !ok { + // bad property, skipping + continue + } + + cardPropertyMap[id] = prop + } + + // if there are properties marked for removal, we delete them + for _, propertyID := range p.DeletedCardProperties { + delete(cardPropertyMap, propertyID) + } + + // if there are properties marked for update, we replace the + // existing ones or add them + for _, newprop := range p.UpdatedCardProperties { + id, ok := newprop["id"].(string) + if !ok { + // bad new property, skipping + continue + } + + cardPropertyMap[id] = newprop + } + + // and finally we flatten and save the updated properties + newCardProperties := []map[string]interface{}{} + for _, p := range cardPropertyMap { + newCardProperties = append(newCardProperties, p) + } + + board.CardProperties = newCardProperties + } + + for key, columnCalculation := range p.UpdatedColumnCalculations { + board.ColumnCalculations[key] = columnCalculation + } + + for _, key := range p.DeletedColumnCalculations { + delete(board.ColumnCalculations, key) + } + + return board +} + +func IsBoardTypeValid(t BoardType) bool { + return t == BoardTypeOpen || t == BoardTypePrivate +} + +func (p *BoardPatch) IsValid() error { + if p.Type != nil && !IsBoardTypeValid(*p.Type) { + return InvalidBoardErr{"invalid-board-type"} + } + + return nil +} + +type InvalidBoardErr struct { + msg string +} + +func (ibe InvalidBoardErr) Error() string { + return ibe.msg +} + +func (b *Board) IsValid() error { + if b.TeamID == "" { + return InvalidBoardErr{"empty-team-id"} + } + + if !IsBoardTypeValid(b.Type) { + return InvalidBoardErr{"invalid-board-type"} + } + return nil +} diff --git a/server/model/boards_and_blocks.go b/server/model/boards_and_blocks.go new file mode 100644 index 000000000..65ca043b2 --- /dev/null +++ b/server/model/boards_and_blocks.go @@ -0,0 +1,164 @@ +package model + +import ( + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/mattermost/focalboard/server/utils" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +var ErrNoBoardsInBoardsAndBlocks = errors.New("at least one board is required") +var ErrNoBlocksInBoardsAndBlocks = errors.New("at least one block is required") +var ErrNoTeamInBoardsAndBlocks = errors.New("team ID cannot be empty") +var ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("board ids and patches need to match") +var ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("block ids and patches need to match") + +type BlockDoesntBelongToAnyBoardErr struct { + blockID string +} + +func (e BlockDoesntBelongToAnyBoardErr) Error() string { + return fmt.Sprintf("block %s doesn't belong to any board", e.blockID) +} + +// BoardsAndBlocks is used to operate over boards and blocks at the +// same time +// swagger:model +type BoardsAndBlocks struct { + // The boards + // required: false + Boards []*Board `json:"boards"` + + // The blocks + // required: false + Blocks []Block `json:"blocks"` +} + +func (bab *BoardsAndBlocks) IsValid() error { + if len(bab.Boards) == 0 { + return ErrNoBoardsInBoardsAndBlocks + } + + if len(bab.Blocks) == 0 { + return ErrNoBlocksInBoardsAndBlocks + } + + boardsMap := map[string]bool{} + for _, board := range bab.Boards { + boardsMap[board.ID] = true + } + + for _, block := range bab.Blocks { + if _, ok := boardsMap[block.BoardID]; !ok { + return BlockDoesntBelongToAnyBoardErr{block.ID} + } + } + return nil +} + +// DeleteBoardsAndBlocks is used to list the boards and blocks to +// delete on a request +// swagger:model +type DeleteBoardsAndBlocks struct { + // The boards + // required: true + Boards []string `json:"boards"` + + // The blocks + // required: true + Blocks []string `json:"blocks"` +} + +func (dbab *DeleteBoardsAndBlocks) IsValid() error { + if len(dbab.Boards) == 0 { + return ErrNoBoardsInBoardsAndBlocks + } + + if len(dbab.Blocks) == 0 { + return ErrNoBlocksInBoardsAndBlocks + } + + return nil +} + +// PatchBoardsAndBlocks is used to patch multiple boards and blocks on +// a single request +// swagger:model +type PatchBoardsAndBlocks struct { + // The board IDs to patch + // required: true + BoardIDs []string `json:"boardIDs"` + + // The board patches + // required: true + BoardPatches []*BoardPatch `json:"boardPatches"` + + // The block IDs to patch + // required: true + BlockIDs []string `json:"blockIDs"` + + // The block patches + // required: true + BlockPatches []*BlockPatch `json:"blockPatches"` +} + +func (dbab *PatchBoardsAndBlocks) IsValid() error { + if len(dbab.BoardIDs) == 0 { + return ErrNoBoardsInBoardsAndBlocks + } + + if len(dbab.BoardIDs) != len(dbab.BoardPatches) { + return ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks + } + + if len(dbab.BlockIDs) == 0 { + return ErrNoBlocksInBoardsAndBlocks + } + + if len(dbab.BlockIDs) != len(dbab.BlockPatches) { + return ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks + } + + return nil +} + +func GenerateBoardsAndBlocksIDs(bab *BoardsAndBlocks, logger *mlog.Logger) (*BoardsAndBlocks, error) { + if err := bab.IsValid(); err != nil { + return nil, err + } + + blocksByBoard := map[string][]Block{} + for _, block := range bab.Blocks { + blocksByBoard[block.BoardID] = append(blocksByBoard[block.BoardID], block) + } + + boards := []*Board{} + blocks := []Block{} + for _, board := range bab.Boards { + newID := utils.NewID(utils.IDTypeBoard) + for _, block := range blocksByBoard[board.ID] { + block.BoardID = newID + blocks = append(blocks, block) + } + + board.ID = newID + boards = append(boards, board) + } + + newBab := &BoardsAndBlocks{ + Boards: boards, + Blocks: GenerateBlockIDs(blocks, logger), + } + + return newBab, nil +} + +func BoardsAndBlocksFromJSON(data io.Reader) *BoardsAndBlocks { + var bab *BoardsAndBlocks + _ = json.NewDecoder(data).Decode(&bab) + return bab +} diff --git a/server/model/boards_and_blocks_test.go b/server/model/boards_and_blocks_test.go new file mode 100644 index 000000000..1a8c01a8a --- /dev/null +++ b/server/model/boards_and_blocks_test.go @@ -0,0 +1,264 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +func TestIsValidBoardsAndBlocks(t *testing.T) { + t.Run("no boards", func(t *testing.T) { + bab := &BoardsAndBlocks{ + Blocks: []Block{ + {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, + {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, + }, + } + + require.ErrorIs(t, bab.IsValid(), ErrNoBoardsInBoardsAndBlocks) + }) + + t.Run("no blocks", func(t *testing.T) { + bab := &BoardsAndBlocks{ + Boards: []*Board{ + {ID: "board-id-1", Type: BoardTypeOpen}, + {ID: "board-id-2", Type: BoardTypePrivate}, + }, + } + + require.ErrorIs(t, bab.IsValid(), ErrNoBlocksInBoardsAndBlocks) + }) + + t.Run("block that doesn't belong to the boards", func(t *testing.T) { + bab := &BoardsAndBlocks{ + Boards: []*Board{ + {ID: "board-id-1", Type: BoardTypeOpen}, + {ID: "board-id-2", Type: BoardTypePrivate}, + }, + Blocks: []Block{ + {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, + {ID: "block-id-3", BoardID: "board-id-3", Type: TypeCard}, + {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, + }, + } + + require.ErrorIs(t, bab.IsValid(), BlockDoesntBelongToAnyBoardErr{"block-id-3"}) + }) + + t.Run("valid boards and blocks", func(t *testing.T) { + bab := &BoardsAndBlocks{ + Boards: []*Board{ + {ID: "board-id-1", Type: BoardTypeOpen}, + {ID: "board-id-2", Type: BoardTypePrivate}, + }, + Blocks: []Block{ + {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, + {ID: "block-id-3", BoardID: "board-id-2", Type: TypeCard}, + {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, + }, + } + + require.NoError(t, bab.IsValid()) + }) +} + +func TestGenerateBoardsAndBlocksIDs(t *testing.T) { + logger, err := mlog.NewLogger() + require.NoError(t, err) + + getBlockByType := func(blocks []Block, blockType BlockType) Block { + for _, b := range blocks { + if b.Type == blockType { + return b + } + } + return Block{} + } + + getBoardByTitle := func(boards []*Board, title string) *Board { + for _, b := range boards { + if b.Title == title { + return b + } + } + return nil + } + + t.Run("invalid boards and blocks", func(t *testing.T) { + bab := &BoardsAndBlocks{ + Blocks: []Block{ + {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, + {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, + }, + } + + rBab, err := GenerateBoardsAndBlocksIDs(bab, logger) + require.Error(t, err) + require.Nil(t, rBab) + }) + + t.Run("correctly generates IDs for all the boards and links the blocks to them, with new IDs too", func(t *testing.T) { + bab := &BoardsAndBlocks{ + Boards: []*Board{ + {ID: "board-id-1", Type: BoardTypeOpen, Title: "board1"}, + {ID: "board-id-2", Type: BoardTypePrivate, Title: "board2"}, + {ID: "board-id-3", Type: BoardTypeOpen, Title: "board3"}, + }, + Blocks: []Block{ + {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, + {ID: "block-id-2", BoardID: "board-id-2", Type: TypeView}, + {ID: "block-id-3", BoardID: "board-id-2", Type: TypeText}, + }, + } + + rBab, err := GenerateBoardsAndBlocksIDs(bab, logger) + require.NoError(t, err) + require.NotNil(t, rBab) + + // all boards and blocks should have refreshed their IDs, and + // blocks should be correctly linked to the new board IDs + board1 := getBoardByTitle(rBab.Boards, "board1") + require.NotNil(t, board1) + require.NotEmpty(t, board1.ID) + require.NotEqual(t, "board-id-1", board1.ID) + board2 := getBoardByTitle(rBab.Boards, "board2") + require.NotNil(t, board2) + require.NotEmpty(t, board2.ID) + require.NotEqual(t, "board-id-2", board2.ID) + board3 := getBoardByTitle(rBab.Boards, "board3") + require.NotNil(t, board3) + require.NotEmpty(t, board3.ID) + require.NotEqual(t, "board-id-3", board3.ID) + + block1 := getBlockByType(rBab.Blocks, TypeCard) + require.NotNil(t, block1) + require.NotEmpty(t, block1.ID) + require.NotEqual(t, "block-id-1", block1.ID) + require.Equal(t, board1.ID, block1.BoardID) + block2 := getBlockByType(rBab.Blocks, TypeView) + require.NotNil(t, block2) + require.NotEmpty(t, block2.ID) + require.NotEqual(t, "block-id-2", block2.ID) + require.Equal(t, board2.ID, block2.BoardID) + block3 := getBlockByType(rBab.Blocks, TypeText) + require.NotNil(t, block3) + require.NotEmpty(t, block3.ID) + require.NotEqual(t, "block-id-3", block3.ID) + require.Equal(t, board2.ID, block3.BoardID) + }) +} + +func TestIsValidPatchBoardsAndBlocks(t *testing.T) { + newTitle := "new title" + newDescription := "new description" + var schema int64 = 1 + + t.Run("no board ids", func(t *testing.T) { + pbab := &PatchBoardsAndBlocks{ + BoardIDs: []string{}, + BlockIDs: []string{"block-id-1"}, + BlockPatches: []*BlockPatch{ + {Title: &newTitle}, + {Schema: &schema}, + }, + } + + require.ErrorIs(t, pbab.IsValid(), ErrNoBoardsInBoardsAndBlocks) + }) + + t.Run("missmatch board IDs and patches", func(t *testing.T) { + pbab := &PatchBoardsAndBlocks{ + BoardIDs: []string{"board-id-1", "board-id-2"}, + BoardPatches: []*BoardPatch{ + {Title: &newTitle}, + }, + BlockIDs: []string{"block-id-1"}, + BlockPatches: []*BlockPatch{ + {Title: &newTitle}, + }, + } + + require.ErrorIs(t, pbab.IsValid(), ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks) + }) + + t.Run("no block ids", func(t *testing.T) { + pbab := &PatchBoardsAndBlocks{ + BoardIDs: []string{"board-id-1", "board-id-2"}, + BoardPatches: []*BoardPatch{ + {Title: &newTitle}, + {Description: &newDescription}, + }, + BlockIDs: []string{}, + } + + require.ErrorIs(t, pbab.IsValid(), ErrNoBlocksInBoardsAndBlocks) + }) + + t.Run("missmatch block IDs and patches", func(t *testing.T) { + pbab := &PatchBoardsAndBlocks{ + BoardIDs: []string{"board-id-1", "board-id-2"}, + BoardPatches: []*BoardPatch{ + {Title: &newTitle}, + {Description: &newDescription}, + }, + BlockIDs: []string{"block-id-1"}, + BlockPatches: []*BlockPatch{ + {Title: &newTitle}, + {Schema: &schema}, + }, + } + + require.ErrorIs(t, pbab.IsValid(), ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks) + }) + + t.Run("valid", func(t *testing.T) { + pbab := &PatchBoardsAndBlocks{ + BoardIDs: []string{"board-id-1", "board-id-2"}, + BoardPatches: []*BoardPatch{ + {Title: &newTitle}, + {Description: &newDescription}, + }, + BlockIDs: []string{"block-id-1"}, + BlockPatches: []*BlockPatch{ + {Title: &newTitle}, + }, + } + + require.NoError(t, pbab.IsValid()) + }) +} + +func TestIsValidDeleteBoardsAndBlocks(t *testing.T) { + /* + TODO fix this + t.Run("no board ids", func(t *testing.T) { + dbab := &DeleteBoardsAndBlocks{ + TeamID: "team-id", + Blocks: []string{"block-id-1"}, + } + + require.ErrorIs(t, dbab.IsValid(), NoBoardsInBoardsAndBlocksErr) + }) + + t.Run("no block ids", func(t *testing.T) { + dbab := &DeleteBoardsAndBlocks{ + TeamID: "team-id", + Boards: []string{"board-id-1", "board-id-2"}, + } + + require.ErrorIs(t, dbab.IsValid(), NoBlocksInBoardsAndBlocksErr) + }) + + t.Run("valid", func(t *testing.T) { + dbab := &DeleteBoardsAndBlocks{ + TeamID: "team-id", + Boards: []string{"board-id-1", "board-id-2"}, + Blocks: []string{"block-id-1"}, + } + + require.NoError(t, dbab.IsValid()) + }) + */ +} diff --git a/server/model/category.go b/server/model/category.go new file mode 100644 index 000000000..22b9140ea --- /dev/null +++ b/server/model/category.go @@ -0,0 +1,57 @@ +package model + +import ( + "strings" + + "github.com/mattermost/focalboard/server/utils" +) + +type Category struct { + ID string `json:"id"` + Name string `json:"name"` + UserID string `json:"userID"` + TeamID string `json:"teamID"` + CreateAt int64 `json:"createAt"` + UpdateAt int64 `json:"updateAt"` + DeleteAt int64 `json:"deleteAt"` +} + +func (c *Category) Hydrate() { + c.ID = utils.NewID(utils.IDTypeNone) + c.CreateAt = utils.GetMillis() + c.UpdateAt = c.CreateAt +} + +func (c *Category) IsValid() error { + if strings.TrimSpace(c.ID) == "" { + return newErrInvalidCategory("category ID cannot be empty") + } + + if strings.TrimSpace(c.Name) == "" { + return newErrInvalidCategory("category name cannot be empty") + } + + if strings.TrimSpace(c.UserID) == "" { + return newErrInvalidCategory("category user ID cannot be empty") + } + + if strings.TrimSpace(c.TeamID) == "" { + return newErrInvalidCategory("category team id ID cannot be empty") + } + + return nil +} + +type ErrInvalidCategory struct { + msg string +} + +func newErrInvalidCategory(msg string) *ErrInvalidCategory { + return &ErrInvalidCategory{ + msg: msg, + } +} + +func (e *ErrInvalidCategory) Error() string { + return e.msg +} diff --git a/server/model/category_blocks.go b/server/model/category_blocks.go new file mode 100644 index 000000000..a8550bed8 --- /dev/null +++ b/server/model/category_blocks.go @@ -0,0 +1,11 @@ +package model + +type CategoryBlocks struct { + Category + BlockIDs []string `json:"blockIDs"` +} + +type BlockCategoryWebsocketData struct { + BlockID string `json:"blockID"` + CategoryID string `json:"categoryID"` +} diff --git a/server/model/database.go b/server/model/database.go new file mode 100644 index 000000000..de430f109 --- /dev/null +++ b/server/model/database.go @@ -0,0 +1,7 @@ +package model + +const ( + SqliteDBType = "sqlite3" + PostgresDBType = "postgres" + MysqlDBType = "mysql" +) diff --git a/server/model/import_export.go b/server/model/import_export.go index 171a04a0d..6eeda6c7e 100644 --- a/server/model/import_export.go +++ b/server/model/import_export.go @@ -33,7 +33,7 @@ type ArchiveLine struct { // ExportArchiveOptions provides options when exporting one or more boards // to an archive. type ExportArchiveOptions struct { - WorkspaceID string + TeamID string // BoardIDs is the list of boards to include in the archive. // Empty slice means export all boards from workspace/team. @@ -42,9 +42,9 @@ type ExportArchiveOptions struct { // ImportArchiveOptions provides options when importing an archive. type ImportArchiveOptions struct { - WorkspaceID string + TeamID string ModifiedBy string - BlockModifier BlockModifier + BoardModifier BoardModifier } // ErrUnsupportedArchiveVersion is an error returned when trying to import an diff --git a/server/model/notification.go b/server/model/notification.go index 2ab80e0f2..1705928c8 100644 --- a/server/model/notification.go +++ b/server/model/notification.go @@ -18,10 +18,6 @@ type NotificationHint struct { // required: true BlockID string `json:"block_id"` - // WorkspaceID is id of workspace the block belongs to - // required: true - WorkspaceID string `json:"workspace_id"` - // ModifiedByID is the id of the user who made the block change ModifiedByID string `json:"modified_by_id"` @@ -41,9 +37,6 @@ func (s *NotificationHint) IsValid() error { if s.BlockID == "" { return ErrInvalidNotificationHint{"missing block id"} } - if s.WorkspaceID == "" { - return ErrInvalidNotificationHint{"missing workspace id"} - } if s.BlockType == "" { return ErrInvalidNotificationHint{"missing block type"} } @@ -57,7 +50,6 @@ func (s *NotificationHint) Copy() *NotificationHint { return &NotificationHint{ BlockType: s.BlockType, BlockID: s.BlockID, - WorkspaceID: s.WorkspaceID, ModifiedByID: s.ModifiedByID, CreateAt: s.CreateAt, NotifyAt: s.NotifyAt, @@ -68,14 +60,12 @@ func (s *NotificationHint) LogClone() interface{} { return struct { BlockType BlockType `json:"block_type"` BlockID string `json:"block_id"` - WorkspaceID string `json:"workspace_id"` ModifiedByID string `json:"modified_by_id"` CreateAt string `json:"create_at"` NotifyAt string `json:"notify_at"` }{ BlockType: s.BlockType, BlockID: s.BlockID, - WorkspaceID: s.WorkspaceID, ModifiedByID: s.ModifiedByID, CreateAt: utils.TimeFromMillis(s.CreateAt).Format(time.StampMilli), NotifyAt: utils.TimeFromMillis(s.NotifyAt).Format(time.StampMilli), diff --git a/server/model/permission.go b/server/model/permission.go new file mode 100644 index 000000000..17ae67cfc --- /dev/null +++ b/server/model/permission.go @@ -0,0 +1,19 @@ +package model + +import ( + mmModel "github.com/mattermost/mattermost-server/v6/model" +) + +var ( + PermissionViewTeam = mmModel.PermissionViewTeam + PermissionViewMembers = mmModel.PermissionViewMembers + PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel + PermissionCreatePrivateChannel = mmModel.PermissionCreatePrivateChannel + PermissionManageBoardType = &mmModel.Permission{Id: "manage_board_type", Name: "", Description: "", Scope: ""} + PermissionDeleteBoard = &mmModel.Permission{Id: "delete_board", Name: "", Description: "", Scope: ""} + PermissionViewBoard = &mmModel.Permission{Id: "view_board", Name: "", Description: "", Scope: ""} + PermissionManageBoardRoles = &mmModel.Permission{Id: "manage_board_roles", Name: "", Description: "", Scope: ""} + PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""} + PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""} + PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""} +) diff --git a/server/model/properties.go b/server/model/properties.go index 39ebbf643..8bb478ce3 100644 --- a/server/model/properties.go +++ b/server/model/properties.go @@ -147,30 +147,10 @@ func (pd PropDef) ParseDate(s string) (string, error) { // schema for all cards within the board. // The result is provided as a map for quick lookup, and the original order is // preserved via the `Index` field. -func ParsePropertySchema(board *Block) (PropSchema, error) { - if board == nil || board.Type != TypeBoard { - return nil, ErrInvalidBoardBlock - } - +func ParsePropertySchema(board *Board) (PropSchema, error) { schema := make(map[string]PropDef) - // cardProperties contains a slice of maps (untyped at this point). - cardPropsIface, ok := board.Fields["cardProperties"] - if !ok { - return schema, nil - } - - cardProps, ok := cardPropsIface.([]interface{}) - if !ok || len(cardProps) == 0 { - return schema, nil - } - - for i, cp := range cardProps { - prop, ok := cp.(map[string]interface{}) - if !ok { - return nil, ErrInvalidPropSchema - } - + for i, prop := range board.CardProperties { pd := PropDef{ ID: getMapString("id", prop), Index: i, diff --git a/server/model/properties_test.go b/server/model/properties_test.go index 8b9eed88c..070e21705 100644 --- a/server/model/properties_test.go +++ b/server/model/properties_test.go @@ -13,14 +13,13 @@ import ( ) func Test_parsePropertySchema(t *testing.T) { - board := &Block{ - ID: utils.NewID(utils.IDTypeBoard), - Type: TypeBoard, - Title: "Test Board", - WorkspaceID: utils.NewID(utils.IDTypeWorkspace), + board := &Board{ + ID: utils.NewID(utils.IDTypeBoard), + Title: "Test Board", + TeamID: utils.NewID(utils.IDTypeTeam), } - err := json.Unmarshal([]byte(fieldsExample), &board.Fields) + err := json.Unmarshal([]byte(cardPropertiesExample), &board.CardProperties) require.NoError(t, err) t.Run("parse schema", func(t *testing.T) { @@ -46,93 +45,74 @@ func Test_parsePropertySchema(t *testing.T) { } const ( - fieldsExample = ` - { - "cardProperties":[ - { - "id":"7c212e78-9345-4c60-81b5-0b0e37ce463f", - "name":"Type", - "options":[ - { - "color":"propColorYellow", - "id":"31da50ca-f1a9-4d21-8636-17dc387c1a23", - "value":"Ad Hoc" - }, - { - "color":"propColorBlue", - "id":"def6317c-ec11-410d-8a6b-ea461320f392", - "value":"Standup" - }, - { - "color":"propColorPurple", - "id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7", - "value":"Weekly Sync" - } - ], - "type":"select" - }, - { - "id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4", - "name":"Summary", - "options":[ - - ], - "type":"text" - }, - { - "id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c", - "name":"Color", - "options":[ - { - "color":"propColorDefault", - "id":"efb0c783-f9ea-4938-8b86-9cf425296cd1", - "value":"RED" - }, - { - "color":"propColorDefault", - "id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311", - "value":"GREEN" - }, - { - "color":"propColorDefault", - "id":"a05bdc80-bd90-45b0-8805-a7e77a4884be", - "value":"BLUE" - } - ], - "type":"select" - }, - { - "id":"aawg1s8rxq8o1bbksxmsmpsdd3r", - "name":"MyTextProp", - "options":[ - - ], - "type":"text" - }, - { - "id":"awdwfigo4kse63bdfp56mzhip6w", - "name":"MyCheckBox", - "options":[ - - ], - "type":"checkbox" - }, - { - "id":"a8spou7if43eo1rqzb9qeq488so", - "name":"MyDate", - "options":[ - - ], - "type":"date" - } - ], - "columnCalculations":[ - - ], - "description":"", - "icon":"🗒️", - "isTemplate":false, - "showDescription":false - } - ` + cardPropertiesExample = `[ + { + "id":"7c212e78-9345-4c60-81b5-0b0e37ce463f", + "name":"Type", + "options":[ + { + "color":"propColorYellow", + "id":"31da50ca-f1a9-4d21-8636-17dc387c1a23", + "value":"Ad Hoc" + }, + { + "color":"propColorBlue", + "id":"def6317c-ec11-410d-8a6b-ea461320f392", + "value":"Standup" + }, + { + "color":"propColorPurple", + "id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7", + "value":"Weekly Sync" + } + ], + "type":"select" + }, + { + "id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4", + "name":"Summary", + "options":[], + "type":"text" + }, + { + "id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c", + "name":"Color", + "options":[ + { + "color":"propColorDefault", + "id":"efb0c783-f9ea-4938-8b86-9cf425296cd1", + "value":"RED" + }, + { + "color":"propColorDefault", + "id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311", + "value":"GREEN" + }, + { + "color":"propColorDefault", + "id":"a05bdc80-bd90-45b0-8805-a7e77a4884be", + "value":"BLUE" + } + ], + "type":"select" + }, + { + "id":"aawg1s8rxq8o1bbksxmsmpsdd3r", + "name":"MyTextProp", + "options":[], + "type":"text" + }, + { + "id":"awdwfigo4kse63bdfp56mzhip6w", + "name":"MyCheckBox", + "options":[], + "type":"checkbox" + }, + { + "id":"a8spou7if43eo1rqzb9qeq488so", + "name":"MyDate", + "options":[], + "type":"date" + } + ]` ) diff --git a/server/model/subscription.go b/server/model/subscription.go index 77337d057..44e904beb 100644 --- a/server/model/subscription.go +++ b/server/model/subscription.go @@ -31,10 +31,6 @@ type Subscription struct { // required: true BlockID string `json:"blockId"` - // WorkspaceID is id of the workspace the block belongs to - // required: true - WorkspaceID string `json:"workspaceId"` - // SubscriberType is the type of the entity (e.g. user, channel) that is subscribing // required: true SubscriberType SubscriberType `json:"subscriberType"` @@ -63,9 +59,6 @@ func (s *Subscription) IsValid() error { if s.BlockID == "" { return ErrInvalidSubscription{"missing block id"} } - if s.WorkspaceID == "" { - return ErrInvalidSubscription{"missing workspace id"} - } if s.BlockType == "" { return ErrInvalidSubscription{"missing block type"} } diff --git a/server/model/team.go b/server/model/team.go new file mode 100644 index 000000000..d6d0ec772 --- /dev/null +++ b/server/model/team.go @@ -0,0 +1,46 @@ +package model + +import ( + "encoding/json" + "io" +) + +// Team is information global to a team +// swagger:model +type Team struct { + // ID of the team + // required: true + ID string `json:"id"` + + // Title of the team + // required: false + Title string `json:"title"` + + // Token required to register new users + // required: true + SignupToken string `json:"signupToken"` + + // Team settings + // required: false + Settings map[string]interface{} `json:"settings"` + + // ID of user who last modified this + // required: true + ModifiedBy string `json:"modifiedBy"` + + // Updated time + // required: true + UpdateAt int64 `json:"updateAt"` +} + +func TeamFromJSON(data io.Reader) *Team { + var team *Team + _ = json.NewDecoder(data).Decode(&team) + return team +} + +func TeamsFromJSON(data io.Reader) []*Team { + var teams []*Team + _ = json.NewDecoder(data).Decode(&teams) + return teams +} diff --git a/server/model/user.go b/server/model/user.go index 9efb102fa..c4c115d0f 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -57,6 +57,8 @@ type User struct { IsBot bool `json:"is_bot"` } +// UserPropPatch is a user property patch +// swagger:model type UserPropPatch struct { // The user prop updated fields // required: false diff --git a/server/model/workspace.go b/server/model/workspace.go deleted file mode 100644 index be8809b62..000000000 --- a/server/model/workspace.go +++ /dev/null @@ -1,45 +0,0 @@ -package model - -// Workspace is information global to a workspace -// swagger:model -type Workspace struct { - // ID of the workspace - // required: true - ID string `json:"id"` - - // Title of the workspace - // required: false - Title string `json:"title"` - - // Token required to register new users - // required: true - SignupToken string `json:"signupToken"` - - // Workspace settings - // required: false - Settings map[string]interface{} `json:"settings"` - - // ID of user who last modified this - // required: true - ModifiedBy string `json:"modifiedBy"` - - // Updated time - // required: true - UpdateAt int64 `json:"updateAt"` -} - -// UserWorkspace is a summary of a single association between -// a user and a workspace -// swagger:model -type UserWorkspace struct { - // ID of the workspace - // required: true - ID string `json:"id"` - - // Title of the workspace - // required: false - Title string `json:"title"` - - // Number of boards in the workspace - BoardCount int `json:"boardCount"` -} diff --git a/server/server/params.go b/server/server/params.go index 8dcbd3ff6..553d30c03 100644 --- a/server/server/params.go +++ b/server/server/params.go @@ -5,6 +5,7 @@ import ( "github.com/mattermost/focalboard/server/services/config" "github.com/mattermost/focalboard/server/services/notify" + "github.com/mattermost/focalboard/server/services/permissions" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/ws" @@ -12,13 +13,14 @@ import ( ) type Params struct { - Cfg *config.Configuration - SingleUserToken string - DBStore store.Store - Logger *mlog.Logger - ServerID string - WSAdapter ws.Adapter - NotifyBackends []notify.Backend + Cfg *config.Configuration + SingleUserToken string + DBStore store.Store + Logger *mlog.Logger + ServerID string + WSAdapter ws.Adapter + NotifyBackends []notify.Backend + PermissionsService permissions.PermissionsService } func (p Params) CheckValid() error { @@ -33,6 +35,10 @@ func (p Params) CheckValid() error { if p.Logger == nil { return ErrServerParam{name: "Logger", issue: "cannot be nil"} } + + if p.PermissionsService == nil { + return ErrServerParam{name: "Permissions", issue: "cannot be nil"} + } return nil } diff --git a/server/server/server.go b/server/server/server.go index 31cb7fb89..375c67b46 100644 --- a/server/server/server.go +++ b/server/server/server.go @@ -74,12 +74,12 @@ func New(params Params) (*Server, error) { return nil, err } - authenticator := auth.New(params.Cfg, params.DBStore) + authenticator := auth.New(params.Cfg, params.DBStore, params.PermissionsService) // if no ws adapter is provided, we spin up a websocket server wsAdapter := params.WSAdapter if wsAdapter == nil { - wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger) + wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger, params.DBStore) } filesBackendSettings := filestore.FileBackendSettings{} @@ -137,18 +137,19 @@ func New(params Params) (*Server, error) { Metrics: metricsService, Notifications: notificationService, Logger: params.Logger, + Permissions: params.PermissionsService, } app := app.New(params.Cfg, wsAdapter, appServices) - focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.Logger, auditService) + focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.PermissionsService, params.Logger, auditService) // Local router for admin APIs localRouter := mux.NewRouter() focalboardAPI.RegisterAdminRoutes(localRouter) - // Init workspace - if _, err := app.GetRootWorkspace(); err != nil { - params.Logger.Error("Unable to get root workspace", mlog.Err(err)) + // Init team + if _, err := app.GetRootTeam(); err != nil { + params.Logger.Error("Unable to get root team", mlog.Err(err)) return nil, err } @@ -202,6 +203,11 @@ func New(params Params) (*Server, error) { server.initHandlers() + if err := app.InitTemplates(); err != nil { + params.Logger.Error("Unable initialize team templates", mlog.Err(err)) + return nil, err + } + return &server, nil } @@ -272,13 +278,13 @@ func (s *Server) Start() error { for blockType, count := range blockCounts { s.metricsService.ObserveBlockCount(blockType, count) } - workspaceCount, err := s.store.GetWorkspaceCount() + teamCount, err := s.store.GetTeamCount() if err != nil { - s.logger.Error("Error updating metrics", mlog.String("group", "workspaces"), mlog.Err(err)) + s.logger.Error("Error updating metrics", mlog.String("group", "teams"), mlog.Err(err)) return } - s.logger.Log(mlog.LvlFBMetrics, "Workspace metrics collected", mlog.Int64("workspace_count", workspaceCount)) - s.metricsService.ObserveWorkspaceCount(workspaceCount) + s.logger.Log(mlog.LvlFBMetrics, "Team metrics collected", mlog.Int64("team_count", teamCount)) + s.metricsService.ObserveTeamCount(teamCount) } // metricsUpdater() Calling this immediately causes integration unit tests to fail. s.metricsUpdaterTask = scheduler.CreateRecurringTask("updateMetrics", metricsUpdater, updateMetricsTaskFrequency) @@ -336,6 +342,8 @@ func (s *Server) Shutdown() error { s.logger.Warn("Error occurred when shutting down notification service", mlog.Err(err)) } + s.app.Shutdown() + defer s.logger.Info("Server.Shutdown") return s.store.Shutdown() @@ -472,13 +480,13 @@ func initTelemetry(opts telemetryOptions) *telemetry.Service { } return m, nil }) - telemetryService.RegisterTracker("workspaces", func() (telemetry.Tracker, error) { - count, err := opts.app.GetWorkspaceCount() + telemetryService.RegisterTracker("teams", func() (telemetry.Tracker, error) { + count, err := opts.app.GetTeamCount() if err != nil { return nil, err } m := map[string]interface{}{ - "workspaces": count, + "teams": count, } return m, nil }) diff --git a/server/services/audit/audit.go b/server/services/audit/audit.go index b6d2772d2..51b196b1e 100644 --- a/server/services/audit/audit.go +++ b/server/services/audit/audit.go @@ -7,15 +7,15 @@ import ( const ( DefMaxQueueSize = 1000 - KeyAPIPath = "api_path" - KeyEvent = "event" - KeyStatus = "status" - KeyUserID = "user_id" - KeySessionID = "session_id" - KeyClient = "client" - KeyIPAddress = "ip_address" - KeyClusterID = "cluster_id" - KeyWorkspaceID = "workspace_id" + KeyAPIPath = "api_path" + KeyEvent = "event" + KeyStatus = "status" + KeyUserID = "user_id" + KeySessionID = "session_id" + KeyClient = "client" + KeyIPAddress = "ip_address" + KeyClusterID = "cluster_id" + KeyTeamID = "team_id" Success = "success" Attempt = "attempt" diff --git a/server/services/auth/email.go b/server/services/auth/email.go index e9b9ec4e0..f8f1d930f 100644 --- a/server/services/auth/email.go +++ b/server/services/auth/email.go @@ -2,7 +2,6 @@ package auth import "regexp" -//nolint:lll var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") // IsEmailValid checks if the email provided passes the required structure and length. diff --git a/server/services/metrics/metrics.go b/server/services/metrics/metrics.go index d6574f6ca..49903c8f9 100644 --- a/server/services/metrics/metrics.go +++ b/server/services/metrics/metrics.go @@ -8,10 +8,10 @@ import ( ) const ( - MetricsNamespace = "focalboard" - MetricsSubsystemBlocks = "blocks" - MetricsSubsystemWorkspaces = "workspaces" - MetricsSubsystemSystem = "system" + MetricsNamespace = "focalboard" + MetricsSubsystemBlocks = "blocks" + MetricsSubsystemTeams = "teams" + MetricsSubsystemSystem = "system" MetricsCloudInstallationLabel = "installationId" ) @@ -38,8 +38,8 @@ type Metrics struct { blocksPatchedCount prometheus.Counter blocksDeletedCount prometheus.Counter - blockCount *prometheus.GaugeVec - workspaceCount prometheus.Gauge + blockCount *prometheus.GaugeVec + teamCount prometheus.Gauge blockLastActivity prometheus.Gauge } @@ -143,14 +143,14 @@ func NewMetrics(info InstanceInfo) *Metrics { }, []string{"BlockType"}) m.registry.MustRegister(m.blockCount) - m.workspaceCount = prometheus.NewGauge(prometheus.GaugeOpts{ + m.teamCount = prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemWorkspaces, - Name: "workspaces_total", - Help: "Total number of workspaces.", + Subsystem: MetricsSubsystemTeams, + Name: "teams_total", + Help: "Total number of teams.", ConstLabels: additionalLabels, }) - m.registry.MustRegister(m.workspaceCount) + m.registry.MustRegister(m.teamCount) m.blockLastActivity = prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: MetricsNamespace, @@ -209,8 +209,8 @@ func (m *Metrics) ObserveBlockCount(blockType string, count int64) { } } -func (m *Metrics) ObserveWorkspaceCount(count int64) { +func (m *Metrics) ObserveTeamCount(count int64) { if m != nil { - m.workspaceCount.Set(float64(count)) + m.teamCount.Set(float64(count)) } } diff --git a/server/services/notify/notifysubscriptions/delivery.go b/server/services/notify/notifysubscriptions/delivery.go index 9e60d0ec9..cab2688f3 100644 --- a/server/services/notify/notifysubscriptions/delivery.go +++ b/server/services/notify/notifysubscriptions/delivery.go @@ -12,6 +12,6 @@ import ( // SubscriptionDelivery provides an interface for delivering subscription notifications to other systems, such as // channels server via plugin API. type SubscriptionDelivery interface { - SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriberType model.SubscriberType, + SubscriptionDeliverSlackAttachments(subscriberID string, subscriberType model.SubscriberType, attachments []*mm_model.SlackAttachment) error } diff --git a/server/services/notify/notifysubscriptions/diff.go b/server/services/notify/notifysubscriptions/diff.go index f1d7055eb..725652086 100644 --- a/server/services/notify/notifysubscriptions/diff.go +++ b/server/services/notify/notifysubscriptions/diff.go @@ -8,14 +8,13 @@ import ( "sort" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) // Diff represents a difference between two versions of a block. type Diff struct { - Board *model.Block + Board *model.Board Card *model.Block Authors StringMap @@ -40,16 +39,15 @@ type PropDiff struct { } type SchemaDiff struct { - Board *model.Block + Board *model.Board OldPropDef *model.PropDef NewPropDef *model.PropDef } type diffGenerator struct { - container store.Container - board *model.Block - card *model.Block + board *model.Board + card *model.Block store Store hint *model.NotificationHint @@ -63,7 +61,7 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) { Limit: 1, Descending: true, } - blocks, err := dg.store.GetBlockHistory(dg.container, dg.hint.BlockID, opts) + blocks, err := dg.store.GetBlockHistory(dg.hint.BlockID, opts) if err != nil { return nil, fmt.Errorf("could not get block for notification: %w", err) } @@ -84,7 +82,10 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) { switch block.Type { case model.TypeBoard: - return dg.generateDiffsForBoard(block, schema) + dg.logger.Warn("generateDiffs for board skipped", mlog.String("block_id", block.ID)) + // TODO: Fix this + // return dg.generateDiffsForBoard(block, schema) + return nil, nil case model.TypeCard: diff, err := dg.generateDiffsForCard(block, schema) if err != nil || diff == nil { @@ -100,27 +101,29 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) { } } -func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model.PropSchema) ([]*Diff, error) { +// TODO: fix this +/* +func (dg *diffGenerator) generateDiffsForBoard(board *model.Board, schema model.PropSchema) ([]*Diff, error) { opts := model.QuerySubtreeOptions{ AfterUpdateAt: dg.lastNotifyAt, } - // find all child blocks of the board that updated since last notify. - blocks, err := dg.store.GetSubTree2(dg.container, board.ID, opts) + find all child blocks of the board that updated since last notify. + blocks, err := dg.store.GetSubTree2(board.ID, board.ID, opts) if err != nil { return nil, fmt.Errorf("could not get subtree for board %s: %w", board.ID, err) } var diffs []*Diff - // generate diff for board title change or description + generate diff for board title change or description boardDiff, err := dg.generateDiffForBlock(board, schema) if err != nil { return nil, fmt.Errorf("could not generate diff for board %s: %w", board.ID, err) } if boardDiff != nil { - // TODO: phase 2 feature (generate schema diffs and add to board diff) goes here. + TODO: phase 2 feature (generate schema diffs and add to board diff) goes here. diffs = append(diffs, boardDiff) } @@ -136,6 +139,7 @@ func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model. } return diffs, nil } +*/ func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.PropSchema) (*Diff, error) { // generate diff for card title change and properties. @@ -148,7 +152,7 @@ func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.Pr opts := model.QuerySubtreeOptions{ AfterUpdateAt: dg.lastNotifyAt, } - blocks, err := dg.store.GetSubTree2(dg.container, card.ID, opts) + blocks, err := dg.store.GetSubTree2(card.BoardID, card.ID, opts) if err != nil { return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err) } @@ -214,7 +218,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode Limit: 1, Descending: true, } - history, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts) + history, err := dg.store.GetBlockHistory(newBlock.ID, opts) if err != nil { return nil, fmt.Errorf("could not get block history for block %s: %w", newBlock.ID, err) } @@ -237,7 +241,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode AfterUpdateAt: dg.lastNotifyAt, Descending: true, } - chgBlocks, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts) + chgBlocks, err := dg.store.GetBlockHistory(newBlock.ID, opts) if err != nil { return nil, fmt.Errorf("error getting block history for block %s: %w", newBlock.ID, err) } diff --git a/server/services/notify/notifysubscriptions/diff2slackattachments.go b/server/services/notify/notifysubscriptions/diff2slackattachments.go index 83fec99e6..98c8f64d2 100644 --- a/server/services/notify/notifysubscriptions/diff2slackattachments.go +++ b/server/services/notify/notifysubscriptions/diff2slackattachments.go @@ -34,7 +34,7 @@ var ( // DiffConvOpts provides options when converting diffs to slack attachments. type DiffConvOpts struct { Language string - MakeCardLink func(block *model.Block, board *model.Block, card *model.Block) string + MakeCardLink func(block *model.Block, board *model.Board, card *model.Block) string Logger *mlog.Logger } @@ -49,7 +49,7 @@ func getTemplate(name string, opts DiffConvOpts, def string) (*template.Template t = template.New(key) if opts.MakeCardLink == nil { - opts.MakeCardLink = func(block *model.Block, _ *model.Block, _ *model.Block) string { + opts.MakeCardLink = func(block *model.Block, _ *model.Board, _ *model.Block) string { return fmt.Sprintf("`%s`", block.Title) } } @@ -160,6 +160,7 @@ func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.Slac mlog.String("card_id", cardDiff.Card.ID), mlog.String("new_block_id", cardDiff.NewBlock.ID), mlog.String("old_block_id", cardDiff.OldBlock.ID), + mlog.Int("childDiffs", len(cardDiff.Diffs)), ) buf.Reset() diff --git a/server/services/notify/notifysubscriptions/notifier.go b/server/services/notify/notifysubscriptions/notifier.go index 40ed07173..098aa7911 100644 --- a/server/services/notify/notifysubscriptions/notifier.go +++ b/server/services/notify/notifysubscriptions/notifier.go @@ -144,12 +144,8 @@ func (n *notifier) notify() { } func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { - c := store.Container{ - WorkspaceID: hint.WorkspaceID, - } - // get the subscriber list - subs, err := n.store.GetSubscribersForBlock(c, hint.BlockID) + subs, err := n.store.GetSubscribersForBlock(hint.BlockID) if err != nil { return err } @@ -162,7 +158,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { oldestNotifiedAt := subs[0].NotifiedAt // need the block's board and card. - board, card, err := n.store.GetBoardAndCardByID(c, hint.BlockID) + board, card, err := n.store.GetBoardAndCardByID(hint.BlockID) if err != nil || board == nil || card == nil { return fmt.Errorf("could not get board & card for block %s: %w", hint.BlockID, err) } @@ -175,7 +171,6 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { ) dg := &diffGenerator{ - container: c, board: board, card: card, store: n.store, @@ -204,8 +199,8 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { opts := DiffConvOpts{ Language: "en", // TODO: use correct language with i18n available on server. - MakeCardLink: func(block *model.Block, board *model.Block, card *model.Block) string { - return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.WorkspaceID, board.ID, card.ID)) + MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string { + return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID)) }, Logger: n.logger, } @@ -236,7 +231,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { mlog.String("subscriber_type", string(sub.SubscriberType)), ) - if err = n.delivery.SubscriptionDeliverSlackAttachments(hint.WorkspaceID, sub.SubscriberID, sub.SubscriberType, attachments); err != nil { + if err = n.delivery.SubscriptionDeliverSlackAttachments(sub.SubscriberID, sub.SubscriberType, attachments); err != nil { merr.Append(fmt.Errorf("cannot deliver notification to subscriber %s [%s]: %w", sub.SubscriberID, sub.SubscriberType, err)) } @@ -262,7 +257,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { } // update the last notified_at for all subscribers since we at least attempted to notify all of them. - err = dg.store.UpdateSubscribersNotifiedAt(dg.container, dg.hint.BlockID, notifiedAt) + err = dg.store.UpdateSubscribersNotifiedAt(dg.hint.BlockID, notifiedAt) if err != nil { merr.Append(fmt.Errorf("could not update subscribers notified_at for block %s: %w", dg.hint.BlockID, err)) } diff --git a/server/services/notify/notifysubscriptions/store.go b/server/services/notify/notifysubscriptions/store.go index c8182cc25..bac406cc7 100644 --- a/server/services/notify/notifysubscriptions/store.go +++ b/server/services/notify/notifysubscriptions/store.go @@ -7,21 +7,20 @@ import ( "time" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" ) type Store interface { - GetBlock(c store.Container, blockID string) (*model.Block, error) - GetBlockHistory(c store.Container, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) - GetSubTree2(c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) - GetBoardAndCardByID(c store.Container, blockID string) (board *model.Block, card *model.Block, err error) + GetBlock(blockID string) (*model.Block, error) + GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) + GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) + GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) GetUserByID(userID string) (*model.User, error) - CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error) - GetSubscribersForBlock(c store.Container, blockID string) ([]*model.Subscriber, error) - GetSubscribersCountForBlock(c store.Container, blockID string) (int, error) - UpdateSubscribersNotifiedAt(c store.Container, blockID string, notifyAt int64) error + CreateSubscription(sub *model.Subscription) (*model.Subscription, error) + GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) + GetSubscribersCountForBlock(blockID string) (int, error) + UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) GetNextNotificationHint(remove bool) (*model.NotificationHint, error) diff --git a/server/services/notify/notifysubscriptions/subscriptions_backend.go b/server/services/notify/notifysubscriptions/subscriptions_backend.go index b18e67e7f..b6e3e98e3 100644 --- a/server/services/notify/notifysubscriptions/subscriptions_backend.go +++ b/server/services/notify/notifysubscriptions/subscriptions_backend.go @@ -9,7 +9,6 @@ import ( "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/notify" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/ws" "github.com/wiggin77/merror" @@ -77,8 +76,6 @@ func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration { switch blockType { case model.TypeCard: return time.Second * time.Duration(b.notifyFreqCardSeconds) - case model.TypeBoard: - return time.Second * time.Duration(b.notifyFreqBoardSeconds) default: return defBlockNotificationFreq } @@ -95,35 +92,30 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { merr := merror.New() var err error - c := store.Container{ - WorkspaceID: evt.Workspace, - } - // if new card added, automatically subscribe the author. if evt.Action == notify.Add && evt.BlockChanged.Type == model.TypeCard { sub := &model.Subscription{ BlockType: model.TypeCard, BlockID: evt.BlockChanged.ID, - WorkspaceID: evt.Workspace, SubscriberType: model.SubTypeUser, SubscriberID: evt.ModifiedByID, } - if sub, err = b.store.CreateSubscription(c, sub); err != nil { + if _, err = b.store.CreateSubscription(sub); err != nil { b.logger.Warn("Cannot subscribe card author to card", mlog.String("card_id", evt.BlockChanged.ID), mlog.Err(err), ) } - b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub) + b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub) } // notify board subscribers - subs, err := b.store.GetSubscribersForBlock(c, evt.Board.ID) + subs, err := b.store.GetSubscribersForBlock(evt.Board.ID) if err != nil { merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err)) } - if err = b.notifySubscribers(subs, evt.Board, evt.ModifiedByID); err != nil { + if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedByID); err != nil { merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err)) } @@ -132,21 +124,21 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { } // notify card subscribers - subs, err = b.store.GetSubscribersForBlock(c, evt.Card.ID) + subs, err = b.store.GetSubscribersForBlock(evt.Card.ID) if err != nil { merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err)) } - if err = b.notifySubscribers(subs, evt.Card, evt.ModifiedByID); err != nil { + if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedByID); err != nil { merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err)) } // notify block subscribers (if/when other types can be subscribed to) if evt.Board.ID != evt.BlockChanged.ID && evt.Card.ID != evt.BlockChanged.ID { - subs, err := b.store.GetSubscribersForBlock(c, evt.BlockChanged.ID) + subs, err := b.store.GetSubscribersForBlock(evt.BlockChanged.ID) if err != nil { merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err)) } - if err := b.notifySubscribers(subs, evt.BlockChanged, evt.ModifiedByID); err != nil { + if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedByID); err != nil { merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err)) } } @@ -154,24 +146,26 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { } // notifySubscribers triggers a change notification for subscribers by writing a notification hint to the database. -func (b *Backend) notifySubscribers(subs []*model.Subscriber, block *model.Block, modifiedByID string) error { +func (b *Backend) notifySubscribers(subs []*model.Subscriber, blockID string, idType model.BlockType, modifiedByID string) error { if len(subs) == 0 { return nil } hint := &model.NotificationHint{ - BlockType: block.Type, - BlockID: block.ID, - WorkspaceID: block.WorkspaceID, + BlockType: idType, + BlockID: blockID, ModifiedByID: modifiedByID, } - hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(block.Type)) + hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(idType)) if err != nil { return fmt.Errorf("cannot upsert notification hint: %w", err) } + if err := b.notifier.onNotifyHint(hint); err != nil { + return err + } - return b.notifier.onNotifyHint(hint) + return nil } // OnMention satisfies the `MentionListener` interface and is called whenever a @mention notification @@ -188,17 +182,13 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) { sub := &model.Subscription{ BlockType: model.TypeCard, BlockID: evt.Card.ID, - WorkspaceID: evt.Workspace, SubscriberType: model.SubTypeUser, SubscriberID: userID, } - c := store.Container{ - WorkspaceID: evt.Workspace, - } var err error - if sub, err = b.store.CreateSubscription(c, sub); err != nil { + if sub, err = b.store.CreateSubscription(sub); err != nil { b.logger.Warn("Cannot subscribe mentioned user to card", mlog.String("user_id", userID), mlog.String("card_id", evt.Card.ID), @@ -206,7 +196,7 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) { ) return } - b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub) + b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub) b.logger.Debug("Subscribed mentioned user to card", mlog.String("user_id", userID), diff --git a/server/services/notify/plugindelivery/mention_deliver.go b/server/services/notify/plugindelivery/mention_deliver.go index 46c06fbfa..325443dd2 100644 --- a/server/services/notify/plugindelivery/mention_deliver.go +++ b/server/services/notify/plugindelivery/mention_deliver.go @@ -14,13 +14,7 @@ import ( // MentionDeliver notifies a user they have been mentioned in a block. func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) { - // determine which team the workspace is associated with - teamID, err := pd.getTeamID(evt) - if err != nil { - return "", fmt.Errorf("cannot determine teamID for block change notification: %w", err) - } - - member, err := teamMemberFromUsername(pd.api, mentionUsername, teamID) + member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID) if err != nil { if isErrNotFound(err) { // not really an error; could just be someone typed "@sometext" @@ -30,16 +24,6 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, } } - // check that user is a member of the channel - _, err = pd.api.GetChannelMember(evt.Workspace, member.UserId) - if err != nil { - if pd.api.IsErrNotFound(err) { - // mentioned user is not a member of the channel; fail silently. - return "", nil - } - return "", fmt.Errorf("cannot fetch channel member for user %s: %w", member.UserId, err) - } - author, err := pd.api.GetUserByID(evt.ModifiedByID) if err != nil { return "", fmt.Errorf("cannot find user: %w", err) @@ -49,7 +33,7 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, if err != nil { return "", fmt.Errorf("cannot get direct channel: %w", err) } - link := utils.MakeCardLink(pd.serverRoot, evt.Workspace, evt.Board.ID, evt.Card.ID) + link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID) post := &model.Post{ UserId: pd.botID, diff --git a/server/services/notify/plugindelivery/plugin_delivery.go b/server/services/notify/plugindelivery/plugin_delivery.go index 4aa8a2044..764b9095d 100644 --- a/server/services/notify/plugindelivery/plugin_delivery.go +++ b/server/services/notify/plugindelivery/plugin_delivery.go @@ -4,7 +4,10 @@ package plugindelivery import ( + "fmt" + "github.com/mattermost/focalboard/server/services/notify" + "github.com/mattermost/focalboard/server/utils" mm_model "github.com/mattermost/mattermost-server/v6/model" ) @@ -52,11 +55,32 @@ func New(botID string, serverRoot string, api PluginAPI) *PluginDelivery { } } -func (pd *PluginDelivery) getTeamID(evt notify.BlockChangeEvent) (string, error) { - // for now, the workspace ID is also the channel ID - channel, err := pd.api.GetChannelByID(evt.Workspace) +func (pd *PluginDelivery) Deliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) error { + member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID) if err != nil { - return "", err + if isErrNotFound(err) { + // not really an error; could just be someone typed "@sometext" + return nil + } else { + return fmt.Errorf("cannot lookup mentioned user: %w", err) + } } - return channel.TeamId, nil + + author, err := pd.api.GetUserByID(evt.ModifiedByID) + if err != nil { + return fmt.Errorf("cannot find user: %w", err) + } + + channel, err := pd.api.GetDirectChannel(member.UserId, pd.botID) + if err != nil { + return fmt.Errorf("cannot get direct channel: %w", err) + } + link := utils.MakeCardLink(pd.serverRoot, evt.TeamID, evt.Board.ID, evt.Card.ID) + + post := &mm_model.Post{ + UserId: pd.botID, + ChannelId: channel.Id, + Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged), + } + return pd.api.CreatePost(post) } diff --git a/server/services/notify/plugindelivery/subscription_deliver.go b/server/services/notify/plugindelivery/subscription_deliver.go index 16aaec60e..77c7f6e76 100644 --- a/server/services/notify/plugindelivery/subscription_deliver.go +++ b/server/services/notify/plugindelivery/subscription_deliver.go @@ -17,10 +17,10 @@ var ( ) // SubscriptionDeliverSlashAttachments notifies a user that changes were made to a block they are subscribed to. -func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriptionType model.SubscriberType, +func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(subscriberID string, subscriptionType model.SubscriberType, attachments []*mm_model.SlackAttachment) error { // check subscriber is member of channel - _, err := pd.api.GetChannelMember(workspaceID, subscriberID) + _, err := pd.api.GetUserByID(subscriberID) if err != nil { if pd.api.IsErrNotFound(err) { // subscriber is not a member of the channel; fail silently. diff --git a/server/services/notify/service.go b/server/services/notify/service.go index 760a0b19e..5735496f7 100644 --- a/server/services/notify/service.go +++ b/server/services/notify/service.go @@ -22,8 +22,8 @@ const ( type BlockChangeEvent struct { Action Action - Workspace string - Board *model.Block + TeamID string + Board *model.Board Card *model.Block BlockChanged *model.Block BlockOld *model.Block @@ -31,7 +31,7 @@ type BlockChangeEvent struct { } type SubscriptionChangeNotifier interface { - BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) + BroadcastSubscriptionChange(subscription *model.Subscription) } // Backend provides an interface for sending notifications. @@ -113,7 +113,7 @@ func (s *Service) BlockChanged(evt BlockChangeEvent) { // BroadcastSubscriptionChange sends a websocket message with details of the changed subscription to all // connected users in the workspace. -func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) { +func (s *Service) BroadcastSubscriptionChange(subscription *model.Subscription) { s.mux.RLock() backends := make([]Backend, len(s.backends)) copy(backends, s.backends) @@ -122,11 +122,10 @@ func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription * for _, backend := range backends { if scn, ok := backend.(SubscriptionChangeNotifier); ok { s.logger.Debug("Delivering subscription change notification", - mlog.String("workspace_id", workspaceID), mlog.String("block_id", subscription.BlockID), mlog.String("subscriber_id", subscription.SubscriberID), ) - scn.BroadcastSubscriptionChange(workspaceID, subscription) + scn.BroadcastSubscriptionChange(subscription) } } } diff --git a/server/services/permissions/localpermissions/helpers_test.go b/server/services/permissions/localpermissions/helpers_test.go new file mode 100644 index 000000000..2fbb61132 --- /dev/null +++ b/server/services/permissions/localpermissions/helpers_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package localpermissions + +import ( + "testing" + + "github.com/mattermost/focalboard/server/model" + permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks" + + mmModel "github.com/mattermost/mattermost-server/v6/model" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +type TestHelper struct { + t *testing.T + ctrl *gomock.Controller + store *permissionsMocks.MockStore + permissions *Service +} + +func SetupTestHelper(t *testing.T) *TestHelper { + ctrl := gomock.NewController(t) + mockStore := permissionsMocks.NewMockStore(ctrl) + + return &TestHelper{ + t: t, + ctrl: ctrl, + store: mockStore, + permissions: New(mockStore, nil), + } +} + +func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) { + for _, p := range hasPermissionTo { + th.t.Run(roleName+" "+p.Id, func(t *testing.T) { + th.store.EXPECT(). + GetMemberForBoard(member.BoardID, member.UserID). + Return(member, nil). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) + assert.True(t, hasPermission) + }) + } + + for _, p := range hasNotPermissionTo { + th.t.Run(roleName+" "+p.Id, func(t *testing.T) { + th.store.EXPECT(). + GetMemberForBoard(member.BoardID, member.UserID). + Return(member, nil). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) + assert.False(t, hasPermission) + }) + } +} diff --git a/server/services/permissions/localpermissions/localpermissions.go b/server/services/permissions/localpermissions/localpermissions.go new file mode 100644 index 000000000..54fa9366b --- /dev/null +++ b/server/services/permissions/localpermissions/localpermissions.go @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package localpermissions + +import ( + "database/sql" + "errors" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/permissions" + + mmModel "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +type Service struct { + store permissions.Store + logger *mlog.Logger +} + +func New(store permissions.Store, logger *mlog.Logger) *Service { + return &Service{ + store: store, + logger: logger, + } +} + +func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool { + if userID == "" || teamID == "" || permission == nil { + return false + } + return true +} + +func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool { + if userID == "" || boardID == "" || permission == nil { + return false + } + + member, err := s.store.GetMemberForBoard(boardID, userID) + if errors.Is(err, sql.ErrNoRows) { + return false + } + if err != nil { + s.logger.Error("error getting member for board", + mlog.String("boardID", boardID), + mlog.String("userID", userID), + mlog.Err(err), + ) + return false + } + + switch permission { + case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard: + return member.SchemeAdmin + case model.PermissionManageBoardCards, model.PermissionManageBoardProperties: + return member.SchemeAdmin || member.SchemeEditor + case model.PermissionViewBoard: + return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer + default: + return false + } +} diff --git a/server/services/permissions/localpermissions/localpermissions_test.go b/server/services/permissions/localpermissions/localpermissions_test.go new file mode 100644 index 000000000..81a9e5677 --- /dev/null +++ b/server/services/permissions/localpermissions/localpermissions_test.go @@ -0,0 +1,144 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package localpermissions + +import ( + "database/sql" + "testing" + + "github.com/mattermost/focalboard/server/model" + + mmModel "github.com/mattermost/mattermost-server/v6/model" + + "github.com/stretchr/testify/assert" +) + +func TestHasPermissionToTeam(t *testing.T) { + th := SetupTestHelper(t) + + t.Run("empty input should always unauthorize", func(t *testing.T) { + assert.False(t, th.permissions.HasPermissionToTeam("", "team-id", model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToTeam("user-id", "", model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToTeam("user-id", "team-id", nil)) + }) + + t.Run("all users have all permissions on teams", func(t *testing.T) { + hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards) + assert.True(t, hasPermission) + }) +} + +func TestHasPermissionToBoard(t *testing.T) { + th := SetupTestHelper(t) + + t.Run("empty input should always unauthorize", func(t *testing.T) { + assert.False(t, th.permissions.HasPermissionToBoard("", "board-id", model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToBoard("user-id", "", model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToBoard("user-id", "board-id", nil)) + }) + + t.Run("nonexistent user", func(t *testing.T) { + userID := "user-id" + boardID := "board-id" + + th.store.EXPECT(). + GetMemberForBoard(boardID, userID). + Return(nil, sql.ErrNoRows). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) + assert.False(t, hasPermission) + }) + + t.Run("board admin", func(t *testing.T) { + member := &model.BoardMember{ + UserID: "user-id", + BoardID: "board-id", + SchemeAdmin: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionViewBoard, + model.PermissionManageBoardProperties, + } + + hasNotPermissionTo := []*mmModel.Permission{} + + th.checkBoardPermissions("admin", member, hasPermissionTo, hasNotPermissionTo) + }) + + t.Run("board editor", func(t *testing.T) { + member := &model.BoardMember{ + UserID: "user-id", + BoardID: "board-id", + SchemeEditor: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardCards, + model.PermissionViewBoard, + model.PermissionManageBoardProperties, + } + + hasNotPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + } + + th.checkBoardPermissions("editor", member, hasPermissionTo, hasNotPermissionTo) + }) + + t.Run("board commenter", func(t *testing.T) { + member := &model.BoardMember{ + UserID: "user-id", + BoardID: "board-id", + SchemeCommenter: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionViewBoard, + } + + hasNotPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionManageBoardProperties, + } + + th.checkBoardPermissions("commenter", member, hasPermissionTo, hasNotPermissionTo) + }) + + t.Run("board viewer", func(t *testing.T) { + member := &model.BoardMember{ + UserID: "user-id", + BoardID: "board-id", + SchemeViewer: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionViewBoard, + } + + hasNotPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionManageBoardProperties, + } + + th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) + }) +} diff --git a/server/services/permissions/mmpermissions/helpers_test.go b/server/services/permissions/mmpermissions/helpers_test.go new file mode 100644 index 000000000..f1a5bcc6f --- /dev/null +++ b/server/services/permissions/mmpermissions/helpers_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package mmpermissions + +import ( + "testing" + + "github.com/mattermost/focalboard/server/model" + mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks" + permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks" + + mmModel "github.com/mattermost/mattermost-server/v6/model" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +type TestHelper struct { + t *testing.T + ctrl *gomock.Controller + store *permissionsMocks.MockStore + api *mmpermissionsMocks.MockAPI + permissions *Service +} + +func SetupTestHelper(t *testing.T) *TestHelper { + ctrl := gomock.NewController(t) + mockStore := permissionsMocks.NewMockStore(ctrl) + mockAPI := mmpermissionsMocks.NewMockAPI(ctrl) + + return &TestHelper{ + t: t, + ctrl: ctrl, + store: mockStore, + api: mockAPI, + permissions: New(mockStore, mockAPI), + } +} + +func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, teamID string, + hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) { + for _, p := range hasPermissionTo { + th.t.Run(roleName+" "+p.Id, func(t *testing.T) { + th.store.EXPECT(). + GetBoard(member.BoardID). + Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil). + Times(1) + + th.api.EXPECT(). + HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam). + Return(true). + Times(1) + + th.store.EXPECT(). + GetMemberForBoard(member.BoardID, member.UserID). + Return(member, nil). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) + assert.True(t, hasPermission) + }) + } + + for _, p := range hasNotPermissionTo { + th.t.Run(roleName+" "+p.Id, func(t *testing.T) { + th.store.EXPECT(). + GetBoard(member.BoardID). + Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil). + Times(1) + + th.api.EXPECT(). + HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam). + Return(true). + Times(1) + + th.store.EXPECT(). + GetMemberForBoard(member.BoardID, member.UserID). + Return(member, nil). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) + assert.False(t, hasPermission) + }) + } +} diff --git a/server/services/permissions/mmpermissions/mmpermissions.go b/server/services/permissions/mmpermissions/mmpermissions.go new file mode 100644 index 000000000..a2c2b6588 --- /dev/null +++ b/server/services/permissions/mmpermissions/mmpermissions.go @@ -0,0 +1,83 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package mmpermissions + +import ( + "database/sql" + "errors" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/permissions" + + mmModel "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/plugin" +) + +type Service struct { + store permissions.Store + api plugin.API +} + +func New(store permissions.Store, api plugin.API) *Service { + return &Service{ + store: store, + api: api, + } +} + +func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool { + if userID == "" || teamID == "" || permission == nil { + return false + } + return s.api.HasPermissionToTeam(userID, teamID, permission) +} + +func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool { + if userID == "" || boardID == "" || permission == nil { + return false + } + + board, err := s.store.GetBoard(boardID) + if errors.Is(err, sql.ErrNoRows) { + return false + } + if err != nil { + s.api.LogError("error getting board", + "boardID", boardID, + "userID", userID, + "error", err, + ) + return false + } + + // we need to check that the user has permission to see the team + // regardless of its local permissions to the board + if !s.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { + return false + } + + member, err := s.store.GetMemberForBoard(boardID, userID) + if errors.Is(err, sql.ErrNoRows) { + return false + } + if err != nil { + s.api.LogError("error getting member for board", + "boardID", boardID, + "userID", userID, + "error", err, + ) + return false + } + + switch permission { + case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard: + return member.SchemeAdmin + case model.PermissionManageBoardCards, model.PermissionManageBoardProperties: + return member.SchemeAdmin || member.SchemeEditor + case model.PermissionViewBoard: + return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer + default: + return false + } +} diff --git a/server/services/permissions/mmpermissions/mmpermissions_test.go b/server/services/permissions/mmpermissions/mmpermissions_test.go new file mode 100644 index 000000000..65995143f --- /dev/null +++ b/server/services/permissions/mmpermissions/mmpermissions_test.go @@ -0,0 +1,217 @@ +//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API +package mmpermissions + +import ( + "database/sql" + "testing" + + "github.com/mattermost/focalboard/server/model" + + mmModel "github.com/mattermost/mattermost-server/v6/model" + + "github.com/stretchr/testify/assert" +) + +const ( + testTeamID = "team-id" + testBoardID = "board-id" + testUserID = "user-id" +) + +func TestHasPermissionsToTeam(t *testing.T) { + th := SetupTestHelper(t) + + t.Run("empty input should always unauthorize", func(t *testing.T) { + assert.False(t, th.permissions.HasPermissionToTeam("", testTeamID, model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToTeam(testUserID, "", model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToTeam(testUserID, testTeamID, nil)) + }) + + t.Run("should authorize if the plugin API does", func(t *testing.T) { + userID := testUserID + teamID := testTeamID + + th.api.EXPECT(). + HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). + Return(true). + Times(1) + + hasPermission := th.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) + assert.True(t, hasPermission) + }) + + t.Run("should not authorize if the plugin API doesn't", func(t *testing.T) { + userID := testUserID + teamID := testTeamID + + th.api.EXPECT(). + HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). + Return(false). + Times(1) + + hasPermission := th.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) + assert.False(t, hasPermission) + }) +} + +// test case for user removed. +func TestHasPermissionToBoard(t *testing.T) { + th := SetupTestHelper(t) + + t.Run("empty input should always unauthorize", func(t *testing.T) { + assert.False(t, th.permissions.HasPermissionToBoard("", testBoardID, model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToBoard(testUserID, "", model.PermissionManageBoardCards)) + assert.False(t, th.permissions.HasPermissionToBoard(testUserID, testBoardID, nil)) + }) + + userID := testUserID + boardID := testBoardID + teamID := testTeamID + + t.Run("nonexistent member", func(t *testing.T) { + th.store.EXPECT(). + GetBoard(boardID). + Return(&model.Board{ID: boardID, TeamID: teamID}, nil). + Times(1) + + th.api.EXPECT(). + HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). + Return(true). + Times(1) + + th.store.EXPECT(). + GetMemberForBoard(boardID, userID). + Return(nil, sql.ErrNoRows). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) + assert.False(t, hasPermission) + }) + + t.Run("nonexistent board", func(t *testing.T) { + th.store.EXPECT(). + GetBoard(boardID). + Return(nil, sql.ErrNoRows). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) + assert.False(t, hasPermission) + }) + + t.Run("user that has been removed from the team", func(t *testing.T) { + member := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeAdmin: true, + } + + th.store.EXPECT(). + GetBoard(boardID). + Return(&model.Board{ID: boardID, TeamID: teamID}, nil). + Times(1) + + th.api.EXPECT(). + HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). + Return(true). + Times(1) + + th.store.EXPECT(). + GetMemberForBoard(member.BoardID, member.UserID). + Return(member, nil). + Times(1) + + hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, model.PermissionViewBoard) + assert.True(t, hasPermission) + }) + + t.Run("board admin", func(t *testing.T) { + member := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeAdmin: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionViewBoard, + model.PermissionManageBoardProperties, + } + + hasNotPermissionTo := []*mmModel.Permission{} + + th.checkBoardPermissions("admin", member, teamID, hasPermissionTo, hasNotPermissionTo) + }) + + t.Run("board editor", func(t *testing.T) { + member := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeEditor: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardCards, + model.PermissionViewBoard, + model.PermissionManageBoardProperties, + } + + hasNotPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + } + + th.checkBoardPermissions("editor", member, teamID, hasPermissionTo, hasNotPermissionTo) + }) + + t.Run("board commenter", func(t *testing.T) { + member := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeCommenter: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionViewBoard, + } + + hasNotPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionManageBoardProperties, + } + + th.checkBoardPermissions("commenter", member, teamID, hasPermissionTo, hasNotPermissionTo) + }) + + t.Run("board viewer", func(t *testing.T) { + member := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeViewer: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionViewBoard, + } + + hasNotPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionManageBoardProperties, + } + + th.checkBoardPermissions("viewer", member, teamID, hasPermissionTo, hasNotPermissionTo) + }) +} diff --git a/server/services/permissions/mmpermissions/mocks/mockpluginapi.go b/server/services/permissions/mmpermissions/mocks/mockpluginapi.go new file mode 100644 index 000000000..f6eacc759 --- /dev/null +++ b/server/services/permissions/mmpermissions/mocks/mockpluginapi.go @@ -0,0 +1,2468 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-server/v6/plugin (interfaces: API) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + io "io" + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost-server/v6/model" +) + +// MockAPI is a mock of API interface. +type MockAPI struct { + ctrl *gomock.Controller + recorder *MockAPIMockRecorder +} + +// MockAPIMockRecorder is the mock recorder for MockAPI. +type MockAPIMockRecorder struct { + mock *MockAPI +} + +// NewMockAPI creates a new mock instance. +func NewMockAPI(ctrl *gomock.Controller) *MockAPI { + mock := &MockAPI{ctrl: ctrl} + mock.recorder = &MockAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAPI) EXPECT() *MockAPIMockRecorder { + return m.recorder +} + +// AddChannelMember mocks base method. +func (m *MockAPI) AddChannelMember(arg0, arg1 string) (*model.ChannelMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddChannelMember", arg0, arg1) + ret0, _ := ret[0].(*model.ChannelMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// AddChannelMember indicates an expected call of AddChannelMember. +func (mr *MockAPIMockRecorder) AddChannelMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddChannelMember", reflect.TypeOf((*MockAPI)(nil).AddChannelMember), arg0, arg1) +} + +// AddReaction mocks base method. +func (m *MockAPI) AddReaction(arg0 *model.Reaction) (*model.Reaction, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddReaction", arg0) + ret0, _ := ret[0].(*model.Reaction) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// AddReaction indicates an expected call of AddReaction. +func (mr *MockAPIMockRecorder) AddReaction(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddReaction", reflect.TypeOf((*MockAPI)(nil).AddReaction), arg0) +} + +// AddUserToChannel mocks base method. +func (m *MockAPI) AddUserToChannel(arg0, arg1, arg2 string) (*model.ChannelMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddUserToChannel", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.ChannelMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// AddUserToChannel indicates an expected call of AddUserToChannel. +func (mr *MockAPIMockRecorder) AddUserToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUserToChannel", reflect.TypeOf((*MockAPI)(nil).AddUserToChannel), arg0, arg1, arg2) +} + +// CopyFileInfos mocks base method. +func (m *MockAPI) CopyFileInfos(arg0 string, arg1 []string) ([]string, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CopyFileInfos", arg0, arg1) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CopyFileInfos indicates an expected call of CopyFileInfos. +func (mr *MockAPIMockRecorder) CopyFileInfos(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyFileInfos", reflect.TypeOf((*MockAPI)(nil).CopyFileInfos), arg0, arg1) +} + +// CreateBot mocks base method. +func (m *MockAPI) CreateBot(arg0 *model.Bot) (*model.Bot, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateBot", arg0) + ret0, _ := ret[0].(*model.Bot) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateBot indicates an expected call of CreateBot. +func (mr *MockAPIMockRecorder) CreateBot(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBot", reflect.TypeOf((*MockAPI)(nil).CreateBot), arg0) +} + +// CreateChannel mocks base method. +func (m *MockAPI) CreateChannel(arg0 *model.Channel) (*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateChannel", arg0) + ret0, _ := ret[0].(*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateChannel indicates an expected call of CreateChannel. +func (mr *MockAPIMockRecorder) CreateChannel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChannel", reflect.TypeOf((*MockAPI)(nil).CreateChannel), arg0) +} + +// CreateChannelSidebarCategory mocks base method. +func (m *MockAPI) CreateChannelSidebarCategory(arg0, arg1 string, arg2 *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateChannelSidebarCategory", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.SidebarCategoryWithChannels) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateChannelSidebarCategory indicates an expected call of CreateChannelSidebarCategory. +func (mr *MockAPIMockRecorder) CreateChannelSidebarCategory(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChannelSidebarCategory", reflect.TypeOf((*MockAPI)(nil).CreateChannelSidebarCategory), arg0, arg1, arg2) +} + +// CreateCommand mocks base method. +func (m *MockAPI) CreateCommand(arg0 *model.Command) (*model.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCommand", arg0) + ret0, _ := ret[0].(*model.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCommand indicates an expected call of CreateCommand. +func (mr *MockAPIMockRecorder) CreateCommand(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCommand", reflect.TypeOf((*MockAPI)(nil).CreateCommand), arg0) +} + +// CreateOAuthApp mocks base method. +func (m *MockAPI) CreateOAuthApp(arg0 *model.OAuthApp) (*model.OAuthApp, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOAuthApp", arg0) + ret0, _ := ret[0].(*model.OAuthApp) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateOAuthApp indicates an expected call of CreateOAuthApp. +func (mr *MockAPIMockRecorder) CreateOAuthApp(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOAuthApp", reflect.TypeOf((*MockAPI)(nil).CreateOAuthApp), arg0) +} + +// CreatePost mocks base method. +func (m *MockAPI) CreatePost(arg0 *model.Post) (*model.Post, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePost", arg0) + ret0, _ := ret[0].(*model.Post) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreatePost indicates an expected call of CreatePost. +func (mr *MockAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockAPI)(nil).CreatePost), arg0) +} + +// CreateTeam mocks base method. +func (m *MockAPI) CreateTeam(arg0 *model.Team) (*model.Team, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTeam", arg0) + ret0, _ := ret[0].(*model.Team) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateTeam indicates an expected call of CreateTeam. +func (mr *MockAPIMockRecorder) CreateTeam(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeam", reflect.TypeOf((*MockAPI)(nil).CreateTeam), arg0) +} + +// CreateTeamMember mocks base method. +func (m *MockAPI) CreateTeamMember(arg0, arg1 string) (*model.TeamMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTeamMember", arg0, arg1) + ret0, _ := ret[0].(*model.TeamMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateTeamMember indicates an expected call of CreateTeamMember. +func (mr *MockAPIMockRecorder) CreateTeamMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMember", reflect.TypeOf((*MockAPI)(nil).CreateTeamMember), arg0, arg1) +} + +// CreateTeamMembers mocks base method. +func (m *MockAPI) CreateTeamMembers(arg0 string, arg1 []string, arg2 string) ([]*model.TeamMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTeamMembers", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.TeamMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateTeamMembers indicates an expected call of CreateTeamMembers. +func (mr *MockAPIMockRecorder) CreateTeamMembers(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMembers", reflect.TypeOf((*MockAPI)(nil).CreateTeamMembers), arg0, arg1, arg2) +} + +// CreateTeamMembersGracefully mocks base method. +func (m *MockAPI) CreateTeamMembersGracefully(arg0 string, arg1 []string, arg2 string) ([]*model.TeamMemberWithError, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTeamMembersGracefully", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.TeamMemberWithError) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateTeamMembersGracefully indicates an expected call of CreateTeamMembersGracefully. +func (mr *MockAPIMockRecorder) CreateTeamMembersGracefully(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMembersGracefully", reflect.TypeOf((*MockAPI)(nil).CreateTeamMembersGracefully), arg0, arg1, arg2) +} + +// CreateUser mocks base method. +func (m *MockAPI) CreateUser(arg0 *model.User) (*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", arg0) + ret0, _ := ret[0].(*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockAPIMockRecorder) CreateUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockAPI)(nil).CreateUser), arg0) +} + +// CreateUserAccessToken mocks base method. +func (m *MockAPI) CreateUserAccessToken(arg0 *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserAccessToken", arg0) + ret0, _ := ret[0].(*model.UserAccessToken) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// CreateUserAccessToken indicates an expected call of CreateUserAccessToken. +func (mr *MockAPIMockRecorder) CreateUserAccessToken(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserAccessToken", reflect.TypeOf((*MockAPI)(nil).CreateUserAccessToken), arg0) +} + +// DeleteChannel mocks base method. +func (m *MockAPI) DeleteChannel(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteChannel", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeleteChannel indicates an expected call of DeleteChannel. +func (mr *MockAPIMockRecorder) DeleteChannel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChannel", reflect.TypeOf((*MockAPI)(nil).DeleteChannel), arg0) +} + +// DeleteChannelMember mocks base method. +func (m *MockAPI) DeleteChannelMember(arg0, arg1 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteChannelMember", arg0, arg1) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeleteChannelMember indicates an expected call of DeleteChannelMember. +func (mr *MockAPIMockRecorder) DeleteChannelMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChannelMember", reflect.TypeOf((*MockAPI)(nil).DeleteChannelMember), arg0, arg1) +} + +// DeleteCommand mocks base method. +func (m *MockAPI) DeleteCommand(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCommand", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCommand indicates an expected call of DeleteCommand. +func (mr *MockAPIMockRecorder) DeleteCommand(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCommand", reflect.TypeOf((*MockAPI)(nil).DeleteCommand), arg0) +} + +// DeleteEphemeralPost mocks base method. +func (m *MockAPI) DeleteEphemeralPost(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteEphemeralPost", arg0, arg1) +} + +// DeleteEphemeralPost indicates an expected call of DeleteEphemeralPost. +func (mr *MockAPIMockRecorder) DeleteEphemeralPost(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEphemeralPost", reflect.TypeOf((*MockAPI)(nil).DeleteEphemeralPost), arg0, arg1) +} + +// DeleteOAuthApp mocks base method. +func (m *MockAPI) DeleteOAuthApp(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteOAuthApp", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeleteOAuthApp indicates an expected call of DeleteOAuthApp. +func (mr *MockAPIMockRecorder) DeleteOAuthApp(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuthApp", reflect.TypeOf((*MockAPI)(nil).DeleteOAuthApp), arg0) +} + +// DeletePost mocks base method. +func (m *MockAPI) DeletePost(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePost", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeletePost indicates an expected call of DeletePost. +func (mr *MockAPIMockRecorder) DeletePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockAPI)(nil).DeletePost), arg0) +} + +// DeletePreferencesForUser mocks base method. +func (m *MockAPI) DeletePreferencesForUser(arg0 string, arg1 []model.Preference) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePreferencesForUser", arg0, arg1) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeletePreferencesForUser indicates an expected call of DeletePreferencesForUser. +func (mr *MockAPIMockRecorder) DeletePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferencesForUser", reflect.TypeOf((*MockAPI)(nil).DeletePreferencesForUser), arg0, arg1) +} + +// DeleteTeam mocks base method. +func (m *MockAPI) DeleteTeam(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteTeam", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeleteTeam indicates an expected call of DeleteTeam. +func (mr *MockAPIMockRecorder) DeleteTeam(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTeam", reflect.TypeOf((*MockAPI)(nil).DeleteTeam), arg0) +} + +// DeleteTeamMember mocks base method. +func (m *MockAPI) DeleteTeamMember(arg0, arg1, arg2 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteTeamMember", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeleteTeamMember indicates an expected call of DeleteTeamMember. +func (mr *MockAPIMockRecorder) DeleteTeamMember(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTeamMember", reflect.TypeOf((*MockAPI)(nil).DeleteTeamMember), arg0, arg1, arg2) +} + +// DeleteUser mocks base method. +func (m *MockAPI) DeleteUser(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUser", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DeleteUser indicates an expected call of DeleteUser. +func (mr *MockAPIMockRecorder) DeleteUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockAPI)(nil).DeleteUser), arg0) +} + +// DisablePlugin mocks base method. +func (m *MockAPI) DisablePlugin(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DisablePlugin", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// DisablePlugin indicates an expected call of DisablePlugin. +func (mr *MockAPIMockRecorder) DisablePlugin(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisablePlugin", reflect.TypeOf((*MockAPI)(nil).DisablePlugin), arg0) +} + +// EnablePlugin mocks base method. +func (m *MockAPI) EnablePlugin(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnablePlugin", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// EnablePlugin indicates an expected call of EnablePlugin. +func (mr *MockAPIMockRecorder) EnablePlugin(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnablePlugin", reflect.TypeOf((*MockAPI)(nil).EnablePlugin), arg0) +} + +// ExecuteSlashCommand mocks base method. +func (m *MockAPI) ExecuteSlashCommand(arg0 *model.CommandArgs) (*model.CommandResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteSlashCommand", arg0) + ret0, _ := ret[0].(*model.CommandResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteSlashCommand indicates an expected call of ExecuteSlashCommand. +func (mr *MockAPIMockRecorder) ExecuteSlashCommand(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSlashCommand", reflect.TypeOf((*MockAPI)(nil).ExecuteSlashCommand), arg0) +} + +// GetBot mocks base method. +func (m *MockAPI) GetBot(arg0 string, arg1 bool) (*model.Bot, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBot", arg0, arg1) + ret0, _ := ret[0].(*model.Bot) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetBot indicates an expected call of GetBot. +func (mr *MockAPIMockRecorder) GetBot(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBot", reflect.TypeOf((*MockAPI)(nil).GetBot), arg0, arg1) +} + +// GetBots mocks base method. +func (m *MockAPI) GetBots(arg0 *model.BotGetOptions) ([]*model.Bot, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBots", arg0) + ret0, _ := ret[0].([]*model.Bot) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetBots indicates an expected call of GetBots. +func (mr *MockAPIMockRecorder) GetBots(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBots", reflect.TypeOf((*MockAPI)(nil).GetBots), arg0) +} + +// GetBundlePath mocks base method. +func (m *MockAPI) GetBundlePath() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBundlePath") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBundlePath indicates an expected call of GetBundlePath. +func (mr *MockAPIMockRecorder) GetBundlePath() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBundlePath", reflect.TypeOf((*MockAPI)(nil).GetBundlePath)) +} + +// GetChannel mocks base method. +func (m *MockAPI) GetChannel(arg0 string) (*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannel", arg0) + ret0, _ := ret[0].(*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannel indicates an expected call of GetChannel. +func (mr *MockAPIMockRecorder) GetChannel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannel", reflect.TypeOf((*MockAPI)(nil).GetChannel), arg0) +} + +// GetChannelByName mocks base method. +func (m *MockAPI) GetChannelByName(arg0, arg1 string, arg2 bool) (*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelByName", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelByName indicates an expected call of GetChannelByName. +func (mr *MockAPIMockRecorder) GetChannelByName(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByName", reflect.TypeOf((*MockAPI)(nil).GetChannelByName), arg0, arg1, arg2) +} + +// GetChannelByNameForTeamName mocks base method. +func (m *MockAPI) GetChannelByNameForTeamName(arg0, arg1 string, arg2 bool) (*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelByNameForTeamName", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelByNameForTeamName indicates an expected call of GetChannelByNameForTeamName. +func (mr *MockAPIMockRecorder) GetChannelByNameForTeamName(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByNameForTeamName", reflect.TypeOf((*MockAPI)(nil).GetChannelByNameForTeamName), arg0, arg1, arg2) +} + +// GetChannelMember mocks base method. +func (m *MockAPI) GetChannelMember(arg0, arg1 string) (*model.ChannelMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelMember", arg0, arg1) + ret0, _ := ret[0].(*model.ChannelMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelMember indicates an expected call of GetChannelMember. +func (mr *MockAPIMockRecorder) GetChannelMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMember", reflect.TypeOf((*MockAPI)(nil).GetChannelMember), arg0, arg1) +} + +// GetChannelMembers mocks base method. +func (m *MockAPI) GetChannelMembers(arg0 string, arg1, arg2 int) (model.ChannelMembers, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelMembers", arg0, arg1, arg2) + ret0, _ := ret[0].(model.ChannelMembers) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelMembers indicates an expected call of GetChannelMembers. +func (mr *MockAPIMockRecorder) GetChannelMembers(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembers", reflect.TypeOf((*MockAPI)(nil).GetChannelMembers), arg0, arg1, arg2) +} + +// GetChannelMembersByIds mocks base method. +func (m *MockAPI) GetChannelMembersByIds(arg0 string, arg1 []string) (model.ChannelMembers, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelMembersByIds", arg0, arg1) + ret0, _ := ret[0].(model.ChannelMembers) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelMembersByIds indicates an expected call of GetChannelMembersByIds. +func (mr *MockAPIMockRecorder) GetChannelMembersByIds(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembersByIds", reflect.TypeOf((*MockAPI)(nil).GetChannelMembersByIds), arg0, arg1) +} + +// GetChannelMembersForUser mocks base method. +func (m *MockAPI) GetChannelMembersForUser(arg0, arg1 string, arg2, arg3 int) ([]*model.ChannelMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelMembersForUser", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]*model.ChannelMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelMembersForUser indicates an expected call of GetChannelMembersForUser. +func (mr *MockAPIMockRecorder) GetChannelMembersForUser(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembersForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelMembersForUser), arg0, arg1, arg2, arg3) +} + +// GetChannelSidebarCategories mocks base method. +func (m *MockAPI) GetChannelSidebarCategories(arg0, arg1 string) (*model.OrderedSidebarCategories, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelSidebarCategories", arg0, arg1) + ret0, _ := ret[0].(*model.OrderedSidebarCategories) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelSidebarCategories indicates an expected call of GetChannelSidebarCategories. +func (mr *MockAPIMockRecorder) GetChannelSidebarCategories(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelSidebarCategories", reflect.TypeOf((*MockAPI)(nil).GetChannelSidebarCategories), arg0, arg1) +} + +// GetChannelStats mocks base method. +func (m *MockAPI) GetChannelStats(arg0 string) (*model.ChannelStats, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelStats", arg0) + ret0, _ := ret[0].(*model.ChannelStats) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelStats indicates an expected call of GetChannelStats. +func (mr *MockAPIMockRecorder) GetChannelStats(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelStats", reflect.TypeOf((*MockAPI)(nil).GetChannelStats), arg0) +} + +// GetChannelsForTeamForUser mocks base method. +func (m *MockAPI) GetChannelsForTeamForUser(arg0, arg1 string, arg2 bool) ([]*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChannelsForTeamForUser", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetChannelsForTeamForUser indicates an expected call of GetChannelsForTeamForUser. +func (mr *MockAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelsForTeamForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelsForTeamForUser), arg0, arg1, arg2) +} + +// GetCommand mocks base method. +func (m *MockAPI) GetCommand(arg0 string) (*model.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommand", arg0) + ret0, _ := ret[0].(*model.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommand indicates an expected call of GetCommand. +func (mr *MockAPIMockRecorder) GetCommand(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommand", reflect.TypeOf((*MockAPI)(nil).GetCommand), arg0) +} + +// GetConfig mocks base method. +func (m *MockAPI) GetConfig() *model.Config { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfig") + ret0, _ := ret[0].(*model.Config) + return ret0 +} + +// GetConfig indicates an expected call of GetConfig. +func (mr *MockAPIMockRecorder) GetConfig() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockAPI)(nil).GetConfig)) +} + +// GetDiagnosticId mocks base method. +func (m *MockAPI) GetDiagnosticId() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDiagnosticId") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetDiagnosticId indicates an expected call of GetDiagnosticId. +func (mr *MockAPIMockRecorder) GetDiagnosticId() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDiagnosticId", reflect.TypeOf((*MockAPI)(nil).GetDiagnosticId)) +} + +// GetDirectChannel mocks base method. +func (m *MockAPI) GetDirectChannel(arg0, arg1 string) (*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDirectChannel", arg0, arg1) + ret0, _ := ret[0].(*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetDirectChannel indicates an expected call of GetDirectChannel. +func (mr *MockAPIMockRecorder) GetDirectChannel(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannel", reflect.TypeOf((*MockAPI)(nil).GetDirectChannel), arg0, arg1) +} + +// GetEmoji mocks base method. +func (m *MockAPI) GetEmoji(arg0 string) (*model.Emoji, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEmoji", arg0) + ret0, _ := ret[0].(*model.Emoji) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetEmoji indicates an expected call of GetEmoji. +func (mr *MockAPIMockRecorder) GetEmoji(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmoji", reflect.TypeOf((*MockAPI)(nil).GetEmoji), arg0) +} + +// GetEmojiByName mocks base method. +func (m *MockAPI) GetEmojiByName(arg0 string) (*model.Emoji, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEmojiByName", arg0) + ret0, _ := ret[0].(*model.Emoji) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetEmojiByName indicates an expected call of GetEmojiByName. +func (mr *MockAPIMockRecorder) GetEmojiByName(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiByName", reflect.TypeOf((*MockAPI)(nil).GetEmojiByName), arg0) +} + +// GetEmojiImage mocks base method. +func (m *MockAPI) GetEmojiImage(arg0 string) ([]byte, string, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEmojiImage", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(*model.AppError) + return ret0, ret1, ret2 +} + +// GetEmojiImage indicates an expected call of GetEmojiImage. +func (mr *MockAPIMockRecorder) GetEmojiImage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiImage", reflect.TypeOf((*MockAPI)(nil).GetEmojiImage), arg0) +} + +// GetEmojiList mocks base method. +func (m *MockAPI) GetEmojiList(arg0 string, arg1, arg2 int) ([]*model.Emoji, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEmojiList", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.Emoji) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetEmojiList indicates an expected call of GetEmojiList. +func (mr *MockAPIMockRecorder) GetEmojiList(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiList", reflect.TypeOf((*MockAPI)(nil).GetEmojiList), arg0, arg1, arg2) +} + +// GetFile mocks base method. +func (m *MockAPI) GetFile(arg0 string) ([]byte, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFile", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetFile indicates an expected call of GetFile. +func (mr *MockAPIMockRecorder) GetFile(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFile", reflect.TypeOf((*MockAPI)(nil).GetFile), arg0) +} + +// GetFileInfo mocks base method. +func (m *MockAPI) GetFileInfo(arg0 string) (*model.FileInfo, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFileInfo", arg0) + ret0, _ := ret[0].(*model.FileInfo) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetFileInfo indicates an expected call of GetFileInfo. +func (mr *MockAPIMockRecorder) GetFileInfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfo", reflect.TypeOf((*MockAPI)(nil).GetFileInfo), arg0) +} + +// GetFileInfos mocks base method. +func (m *MockAPI) GetFileInfos(arg0, arg1 int, arg2 *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFileInfos", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.FileInfo) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetFileInfos indicates an expected call of GetFileInfos. +func (mr *MockAPIMockRecorder) GetFileInfos(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfos", reflect.TypeOf((*MockAPI)(nil).GetFileInfos), arg0, arg1, arg2) +} + +// GetFileLink mocks base method. +func (m *MockAPI) GetFileLink(arg0 string) (string, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFileLink", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetFileLink indicates an expected call of GetFileLink. +func (mr *MockAPIMockRecorder) GetFileLink(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileLink", reflect.TypeOf((*MockAPI)(nil).GetFileLink), arg0) +} + +// GetGroup mocks base method. +func (m *MockAPI) GetGroup(arg0 string) (*model.Group, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGroup", arg0) + ret0, _ := ret[0].(*model.Group) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetGroup indicates an expected call of GetGroup. +func (mr *MockAPIMockRecorder) GetGroup(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroup", reflect.TypeOf((*MockAPI)(nil).GetGroup), arg0) +} + +// GetGroupByName mocks base method. +func (m *MockAPI) GetGroupByName(arg0 string) (*model.Group, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGroupByName", arg0) + ret0, _ := ret[0].(*model.Group) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetGroupByName indicates an expected call of GetGroupByName. +func (mr *MockAPIMockRecorder) GetGroupByName(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockAPI)(nil).GetGroupByName), arg0) +} + +// GetGroupChannel mocks base method. +func (m *MockAPI) GetGroupChannel(arg0 []string) (*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGroupChannel", arg0) + ret0, _ := ret[0].(*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetGroupChannel indicates an expected call of GetGroupChannel. +func (mr *MockAPIMockRecorder) GetGroupChannel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupChannel", reflect.TypeOf((*MockAPI)(nil).GetGroupChannel), arg0) +} + +// GetGroupMemberUsers mocks base method. +func (m *MockAPI) GetGroupMemberUsers(arg0 string, arg1, arg2 int) ([]*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGroupMemberUsers", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetGroupMemberUsers indicates an expected call of GetGroupMemberUsers. +func (mr *MockAPIMockRecorder) GetGroupMemberUsers(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMemberUsers", reflect.TypeOf((*MockAPI)(nil).GetGroupMemberUsers), arg0, arg1, arg2) +} + +// GetGroupsBySource mocks base method. +func (m *MockAPI) GetGroupsBySource(arg0 model.GroupSource) ([]*model.Group, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGroupsBySource", arg0) + ret0, _ := ret[0].([]*model.Group) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetGroupsBySource indicates an expected call of GetGroupsBySource. +func (mr *MockAPIMockRecorder) GetGroupsBySource(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsBySource", reflect.TypeOf((*MockAPI)(nil).GetGroupsBySource), arg0) +} + +// GetGroupsForUser mocks base method. +func (m *MockAPI) GetGroupsForUser(arg0 string) ([]*model.Group, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGroupsForUser", arg0) + ret0, _ := ret[0].([]*model.Group) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetGroupsForUser indicates an expected call of GetGroupsForUser. +func (mr *MockAPIMockRecorder) GetGroupsForUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsForUser", reflect.TypeOf((*MockAPI)(nil).GetGroupsForUser), arg0) +} + +// GetLDAPUserAttributes mocks base method. +func (m *MockAPI) GetLDAPUserAttributes(arg0 string, arg1 []string) (map[string]string, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLDAPUserAttributes", arg0, arg1) + ret0, _ := ret[0].(map[string]string) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetLDAPUserAttributes indicates an expected call of GetLDAPUserAttributes. +func (mr *MockAPIMockRecorder) GetLDAPUserAttributes(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLDAPUserAttributes", reflect.TypeOf((*MockAPI)(nil).GetLDAPUserAttributes), arg0, arg1) +} + +// GetLicense mocks base method. +func (m *MockAPI) GetLicense() *model.License { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLicense") + ret0, _ := ret[0].(*model.License) + return ret0 +} + +// GetLicense indicates an expected call of GetLicense. +func (mr *MockAPIMockRecorder) GetLicense() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicense", reflect.TypeOf((*MockAPI)(nil).GetLicense)) +} + +// GetOAuthApp mocks base method. +func (m *MockAPI) GetOAuthApp(arg0 string) (*model.OAuthApp, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOAuthApp", arg0) + ret0, _ := ret[0].(*model.OAuthApp) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetOAuthApp indicates an expected call of GetOAuthApp. +func (mr *MockAPIMockRecorder) GetOAuthApp(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuthApp", reflect.TypeOf((*MockAPI)(nil).GetOAuthApp), arg0) +} + +// GetPluginConfig mocks base method. +func (m *MockAPI) GetPluginConfig() map[string]interface{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPluginConfig") + ret0, _ := ret[0].(map[string]interface{}) + return ret0 +} + +// GetPluginConfig indicates an expected call of GetPluginConfig. +func (mr *MockAPIMockRecorder) GetPluginConfig() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginConfig", reflect.TypeOf((*MockAPI)(nil).GetPluginConfig)) +} + +// GetPluginStatus mocks base method. +func (m *MockAPI) GetPluginStatus(arg0 string) (*model.PluginStatus, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPluginStatus", arg0) + ret0, _ := ret[0].(*model.PluginStatus) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPluginStatus indicates an expected call of GetPluginStatus. +func (mr *MockAPIMockRecorder) GetPluginStatus(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginStatus", reflect.TypeOf((*MockAPI)(nil).GetPluginStatus), arg0) +} + +// GetPlugins mocks base method. +func (m *MockAPI) GetPlugins() ([]*model.Manifest, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPlugins") + ret0, _ := ret[0].([]*model.Manifest) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPlugins indicates an expected call of GetPlugins. +func (mr *MockAPIMockRecorder) GetPlugins() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlugins", reflect.TypeOf((*MockAPI)(nil).GetPlugins)) +} + +// GetPost mocks base method. +func (m *MockAPI) GetPost(arg0 string) (*model.Post, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPost", arg0) + ret0, _ := ret[0].(*model.Post) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPost indicates an expected call of GetPost. +func (mr *MockAPIMockRecorder) GetPost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPost", reflect.TypeOf((*MockAPI)(nil).GetPost), arg0) +} + +// GetPostThread mocks base method. +func (m *MockAPI) GetPostThread(arg0 string) (*model.PostList, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPostThread", arg0) + ret0, _ := ret[0].(*model.PostList) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPostThread indicates an expected call of GetPostThread. +func (mr *MockAPIMockRecorder) GetPostThread(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostThread", reflect.TypeOf((*MockAPI)(nil).GetPostThread), arg0) +} + +// GetPostsAfter mocks base method. +func (m *MockAPI) GetPostsAfter(arg0, arg1 string, arg2, arg3 int) (*model.PostList, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPostsAfter", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*model.PostList) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPostsAfter indicates an expected call of GetPostsAfter. +func (mr *MockAPIMockRecorder) GetPostsAfter(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsAfter", reflect.TypeOf((*MockAPI)(nil).GetPostsAfter), arg0, arg1, arg2, arg3) +} + +// GetPostsBefore mocks base method. +func (m *MockAPI) GetPostsBefore(arg0, arg1 string, arg2, arg3 int) (*model.PostList, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPostsBefore", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*model.PostList) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPostsBefore indicates an expected call of GetPostsBefore. +func (mr *MockAPIMockRecorder) GetPostsBefore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsBefore", reflect.TypeOf((*MockAPI)(nil).GetPostsBefore), arg0, arg1, arg2, arg3) +} + +// GetPostsForChannel mocks base method. +func (m *MockAPI) GetPostsForChannel(arg0 string, arg1, arg2 int) (*model.PostList, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPostsForChannel", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.PostList) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPostsForChannel indicates an expected call of GetPostsForChannel. +func (mr *MockAPIMockRecorder) GetPostsForChannel(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsForChannel", reflect.TypeOf((*MockAPI)(nil).GetPostsForChannel), arg0, arg1, arg2) +} + +// GetPostsSince mocks base method. +func (m *MockAPI) GetPostsSince(arg0 string, arg1 int64) (*model.PostList, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPostsSince", arg0, arg1) + ret0, _ := ret[0].(*model.PostList) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPostsSince indicates an expected call of GetPostsSince. +func (mr *MockAPIMockRecorder) GetPostsSince(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsSince", reflect.TypeOf((*MockAPI)(nil).GetPostsSince), arg0, arg1) +} + +// GetPreferencesForUser mocks base method. +func (m *MockAPI) GetPreferencesForUser(arg0 string) ([]model.Preference, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPreferencesForUser", arg0) + ret0, _ := ret[0].([]model.Preference) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPreferencesForUser indicates an expected call of GetPreferencesForUser. +func (mr *MockAPIMockRecorder) GetPreferencesForUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesForUser", reflect.TypeOf((*MockAPI)(nil).GetPreferencesForUser), arg0) +} + +// GetProfileImage mocks base method. +func (m *MockAPI) GetProfileImage(arg0 string) ([]byte, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProfileImage", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetProfileImage indicates an expected call of GetProfileImage. +func (mr *MockAPIMockRecorder) GetProfileImage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileImage", reflect.TypeOf((*MockAPI)(nil).GetProfileImage), arg0) +} + +// GetPublicChannelsForTeam mocks base method. +func (m *MockAPI) GetPublicChannelsForTeam(arg0 string, arg1, arg2 int) ([]*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPublicChannelsForTeam", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetPublicChannelsForTeam indicates an expected call of GetPublicChannelsForTeam. +func (mr *MockAPIMockRecorder) GetPublicChannelsForTeam(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicChannelsForTeam", reflect.TypeOf((*MockAPI)(nil).GetPublicChannelsForTeam), arg0, arg1, arg2) +} + +// GetReactions mocks base method. +func (m *MockAPI) GetReactions(arg0 string) ([]*model.Reaction, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReactions", arg0) + ret0, _ := ret[0].([]*model.Reaction) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetReactions indicates an expected call of GetReactions. +func (mr *MockAPIMockRecorder) GetReactions(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReactions", reflect.TypeOf((*MockAPI)(nil).GetReactions), arg0) +} + +// GetServerVersion mocks base method. +func (m *MockAPI) GetServerVersion() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServerVersion") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetServerVersion indicates an expected call of GetServerVersion. +func (mr *MockAPIMockRecorder) GetServerVersion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerVersion", reflect.TypeOf((*MockAPI)(nil).GetServerVersion)) +} + +// GetSession mocks base method. +func (m *MockAPI) GetSession(arg0 string) (*model.Session, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSession", arg0) + ret0, _ := ret[0].(*model.Session) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetSession indicates an expected call of GetSession. +func (mr *MockAPIMockRecorder) GetSession(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockAPI)(nil).GetSession), arg0) +} + +// GetSystemInstallDate mocks base method. +func (m *MockAPI) GetSystemInstallDate() (int64, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSystemInstallDate") + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetSystemInstallDate indicates an expected call of GetSystemInstallDate. +func (mr *MockAPIMockRecorder) GetSystemInstallDate() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSystemInstallDate", reflect.TypeOf((*MockAPI)(nil).GetSystemInstallDate)) +} + +// GetTeam mocks base method. +func (m *MockAPI) GetTeam(arg0 string) (*model.Team, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeam", arg0) + ret0, _ := ret[0].(*model.Team) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeam indicates an expected call of GetTeam. +func (mr *MockAPIMockRecorder) GetTeam(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeam", reflect.TypeOf((*MockAPI)(nil).GetTeam), arg0) +} + +// GetTeamByName mocks base method. +func (m *MockAPI) GetTeamByName(arg0 string) (*model.Team, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamByName", arg0) + ret0, _ := ret[0].(*model.Team) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamByName indicates an expected call of GetTeamByName. +func (mr *MockAPIMockRecorder) GetTeamByName(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamByName", reflect.TypeOf((*MockAPI)(nil).GetTeamByName), arg0) +} + +// GetTeamIcon mocks base method. +func (m *MockAPI) GetTeamIcon(arg0 string) ([]byte, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamIcon", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamIcon indicates an expected call of GetTeamIcon. +func (mr *MockAPIMockRecorder) GetTeamIcon(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamIcon", reflect.TypeOf((*MockAPI)(nil).GetTeamIcon), arg0) +} + +// GetTeamMember mocks base method. +func (m *MockAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamMember", arg0, arg1) + ret0, _ := ret[0].(*model.TeamMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamMember indicates an expected call of GetTeamMember. +func (mr *MockAPIMockRecorder) GetTeamMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMember", reflect.TypeOf((*MockAPI)(nil).GetTeamMember), arg0, arg1) +} + +// GetTeamMembers mocks base method. +func (m *MockAPI) GetTeamMembers(arg0 string, arg1, arg2 int) ([]*model.TeamMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamMembers", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.TeamMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamMembers indicates an expected call of GetTeamMembers. +func (mr *MockAPIMockRecorder) GetTeamMembers(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembers", reflect.TypeOf((*MockAPI)(nil).GetTeamMembers), arg0, arg1, arg2) +} + +// GetTeamMembersForUser mocks base method. +func (m *MockAPI) GetTeamMembersForUser(arg0 string, arg1, arg2 int) ([]*model.TeamMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamMembersForUser", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.TeamMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamMembersForUser indicates an expected call of GetTeamMembersForUser. +func (mr *MockAPIMockRecorder) GetTeamMembersForUser(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembersForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamMembersForUser), arg0, arg1, arg2) +} + +// GetTeamStats mocks base method. +func (m *MockAPI) GetTeamStats(arg0 string) (*model.TeamStats, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamStats", arg0) + ret0, _ := ret[0].(*model.TeamStats) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamStats indicates an expected call of GetTeamStats. +func (mr *MockAPIMockRecorder) GetTeamStats(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamStats", reflect.TypeOf((*MockAPI)(nil).GetTeamStats), arg0) +} + +// GetTeams mocks base method. +func (m *MockAPI) GetTeams() ([]*model.Team, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeams") + ret0, _ := ret[0].([]*model.Team) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeams indicates an expected call of GetTeams. +func (mr *MockAPIMockRecorder) GetTeams() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeams", reflect.TypeOf((*MockAPI)(nil).GetTeams)) +} + +// GetTeamsForUser mocks base method. +func (m *MockAPI) GetTeamsForUser(arg0 string) ([]*model.Team, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamsForUser", arg0) + ret0, _ := ret[0].([]*model.Team) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamsForUser indicates an expected call of GetTeamsForUser. +func (mr *MockAPIMockRecorder) GetTeamsForUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamsForUser), arg0) +} + +// GetTeamsUnreadForUser mocks base method. +func (m *MockAPI) GetTeamsUnreadForUser(arg0 string) ([]*model.TeamUnread, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamsUnreadForUser", arg0) + ret0, _ := ret[0].([]*model.TeamUnread) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetTeamsUnreadForUser indicates an expected call of GetTeamsUnreadForUser. +func (mr *MockAPIMockRecorder) GetTeamsUnreadForUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsUnreadForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamsUnreadForUser), arg0) +} + +// GetTelemetryId mocks base method. +func (m *MockAPI) GetTelemetryId() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTelemetryId") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetTelemetryId indicates an expected call of GetTelemetryId. +func (mr *MockAPIMockRecorder) GetTelemetryId() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryId", reflect.TypeOf((*MockAPI)(nil).GetTelemetryId)) +} + +// GetUnsanitizedConfig mocks base method. +func (m *MockAPI) GetUnsanitizedConfig() *model.Config { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnsanitizedConfig") + ret0, _ := ret[0].(*model.Config) + return ret0 +} + +// GetUnsanitizedConfig indicates an expected call of GetUnsanitizedConfig. +func (mr *MockAPIMockRecorder) GetUnsanitizedConfig() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnsanitizedConfig", reflect.TypeOf((*MockAPI)(nil).GetUnsanitizedConfig)) +} + +// GetUser mocks base method. +func (m *MockAPI) GetUser(arg0 string) (*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUser", arg0) + ret0, _ := ret[0].(*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser. +func (mr *MockAPIMockRecorder) GetUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockAPI)(nil).GetUser), arg0) +} + +// GetUserByEmail mocks base method. +func (m *MockAPI) GetUserByEmail(arg0 string) (*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByEmail", arg0) + ret0, _ := ret[0].(*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUserByEmail indicates an expected call of GetUserByEmail. +func (mr *MockAPIMockRecorder) GetUserByEmail(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockAPI)(nil).GetUserByEmail), arg0) +} + +// GetUserByUsername mocks base method. +func (m *MockAPI) GetUserByUsername(arg0 string) (*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByUsername", arg0) + ret0, _ := ret[0].(*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUserByUsername indicates an expected call of GetUserByUsername. +func (mr *MockAPIMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockAPI)(nil).GetUserByUsername), arg0) +} + +// GetUserStatus mocks base method. +func (m *MockAPI) GetUserStatus(arg0 string) (*model.Status, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserStatus", arg0) + ret0, _ := ret[0].(*model.Status) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUserStatus indicates an expected call of GetUserStatus. +func (mr *MockAPIMockRecorder) GetUserStatus(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatus", reflect.TypeOf((*MockAPI)(nil).GetUserStatus), arg0) +} + +// GetUserStatusesByIds mocks base method. +func (m *MockAPI) GetUserStatusesByIds(arg0 []string) ([]*model.Status, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserStatusesByIds", arg0) + ret0, _ := ret[0].([]*model.Status) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUserStatusesByIds indicates an expected call of GetUserStatusesByIds. +func (mr *MockAPIMockRecorder) GetUserStatusesByIds(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusesByIds", reflect.TypeOf((*MockAPI)(nil).GetUserStatusesByIds), arg0) +} + +// GetUsers mocks base method. +func (m *MockAPI) GetUsers(arg0 *model.UserGetOptions) ([]*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsers", arg0) + ret0, _ := ret[0].([]*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUsers indicates an expected call of GetUsers. +func (mr *MockAPIMockRecorder) GetUsers(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsers", reflect.TypeOf((*MockAPI)(nil).GetUsers), arg0) +} + +// GetUsersByUsernames mocks base method. +func (m *MockAPI) GetUsersByUsernames(arg0 []string) ([]*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsersByUsernames", arg0) + ret0, _ := ret[0].([]*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUsersByUsernames indicates an expected call of GetUsersByUsernames. +func (mr *MockAPIMockRecorder) GetUsersByUsernames(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByUsernames", reflect.TypeOf((*MockAPI)(nil).GetUsersByUsernames), arg0) +} + +// GetUsersInChannel mocks base method. +func (m *MockAPI) GetUsersInChannel(arg0, arg1 string, arg2, arg3 int) ([]*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsersInChannel", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUsersInChannel indicates an expected call of GetUsersInChannel. +func (mr *MockAPIMockRecorder) GetUsersInChannel(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersInChannel", reflect.TypeOf((*MockAPI)(nil).GetUsersInChannel), arg0, arg1, arg2, arg3) +} + +// GetUsersInTeam mocks base method. +func (m *MockAPI) GetUsersInTeam(arg0 string, arg1, arg2 int) ([]*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsersInTeam", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUsersInTeam indicates an expected call of GetUsersInTeam. +func (mr *MockAPIMockRecorder) GetUsersInTeam(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersInTeam", reflect.TypeOf((*MockAPI)(nil).GetUsersInTeam), arg0, arg1, arg2) +} + +// HasPermissionTo mocks base method. +func (m *MockAPI) HasPermissionTo(arg0 string, arg1 *model.Permission) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasPermissionTo", arg0, arg1) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasPermissionTo indicates an expected call of HasPermissionTo. +func (mr *MockAPIMockRecorder) HasPermissionTo(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionTo", reflect.TypeOf((*MockAPI)(nil).HasPermissionTo), arg0, arg1) +} + +// HasPermissionToChannel mocks base method. +func (m *MockAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasPermissionToChannel", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasPermissionToChannel indicates an expected call of HasPermissionToChannel. +func (mr *MockAPIMockRecorder) HasPermissionToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToChannel", reflect.TypeOf((*MockAPI)(nil).HasPermissionToChannel), arg0, arg1, arg2) +} + +// HasPermissionToTeam mocks base method. +func (m *MockAPI) HasPermissionToTeam(arg0, arg1 string, arg2 *model.Permission) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasPermissionToTeam", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasPermissionToTeam indicates an expected call of HasPermissionToTeam. +func (mr *MockAPIMockRecorder) HasPermissionToTeam(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToTeam", reflect.TypeOf((*MockAPI)(nil).HasPermissionToTeam), arg0, arg1, arg2) +} + +// InstallPlugin mocks base method. +func (m *MockAPI) InstallPlugin(arg0 io.Reader, arg1 bool) (*model.Manifest, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstallPlugin", arg0, arg1) + ret0, _ := ret[0].(*model.Manifest) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// InstallPlugin indicates an expected call of InstallPlugin. +func (mr *MockAPIMockRecorder) InstallPlugin(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallPlugin", reflect.TypeOf((*MockAPI)(nil).InstallPlugin), arg0, arg1) +} + +// KVCompareAndDelete mocks base method. +func (m *MockAPI) KVCompareAndDelete(arg0 string, arg1 []byte) (bool, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVCompareAndDelete", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVCompareAndDelete indicates an expected call of KVCompareAndDelete. +func (mr *MockAPIMockRecorder) KVCompareAndDelete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVCompareAndDelete", reflect.TypeOf((*MockAPI)(nil).KVCompareAndDelete), arg0, arg1) +} + +// KVCompareAndSet mocks base method. +func (m *MockAPI) KVCompareAndSet(arg0 string, arg1, arg2 []byte) (bool, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVCompareAndSet", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVCompareAndSet indicates an expected call of KVCompareAndSet. +func (mr *MockAPIMockRecorder) KVCompareAndSet(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVCompareAndSet", reflect.TypeOf((*MockAPI)(nil).KVCompareAndSet), arg0, arg1, arg2) +} + +// KVDelete mocks base method. +func (m *MockAPI) KVDelete(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVDelete", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// KVDelete indicates an expected call of KVDelete. +func (mr *MockAPIMockRecorder) KVDelete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVDelete", reflect.TypeOf((*MockAPI)(nil).KVDelete), arg0) +} + +// KVDeleteAll mocks base method. +func (m *MockAPI) KVDeleteAll() *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVDeleteAll") + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// KVDeleteAll indicates an expected call of KVDeleteAll. +func (mr *MockAPIMockRecorder) KVDeleteAll() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVDeleteAll", reflect.TypeOf((*MockAPI)(nil).KVDeleteAll)) +} + +// KVGet mocks base method. +func (m *MockAPI) KVGet(arg0 string) ([]byte, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVGet", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVGet indicates an expected call of KVGet. +func (mr *MockAPIMockRecorder) KVGet(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVGet", reflect.TypeOf((*MockAPI)(nil).KVGet), arg0) +} + +// KVList mocks base method. +func (m *MockAPI) KVList(arg0, arg1 int) ([]string, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVList", arg0, arg1) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVList indicates an expected call of KVList. +func (mr *MockAPIMockRecorder) KVList(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVList", reflect.TypeOf((*MockAPI)(nil).KVList), arg0, arg1) +} + +// KVSet mocks base method. +func (m *MockAPI) KVSet(arg0 string, arg1 []byte) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVSet", arg0, arg1) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// KVSet indicates an expected call of KVSet. +func (mr *MockAPIMockRecorder) KVSet(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSet", reflect.TypeOf((*MockAPI)(nil).KVSet), arg0, arg1) +} + +// KVSetWithExpiry mocks base method. +func (m *MockAPI) KVSetWithExpiry(arg0 string, arg1 []byte, arg2 int64) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVSetWithExpiry", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// KVSetWithExpiry indicates an expected call of KVSetWithExpiry. +func (mr *MockAPIMockRecorder) KVSetWithExpiry(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithExpiry", reflect.TypeOf((*MockAPI)(nil).KVSetWithExpiry), arg0, arg1, arg2) +} + +// KVSetWithOptions mocks base method. +func (m *MockAPI) KVSetWithOptions(arg0 string, arg1 []byte, arg2 model.PluginKVSetOptions) (bool, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVSetWithOptions", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVSetWithOptions indicates an expected call of KVSetWithOptions. +func (mr *MockAPIMockRecorder) KVSetWithOptions(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithOptions", reflect.TypeOf((*MockAPI)(nil).KVSetWithOptions), arg0, arg1, arg2) +} + +// ListBuiltInCommands mocks base method. +func (m *MockAPI) ListBuiltInCommands() ([]*model.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListBuiltInCommands") + ret0, _ := ret[0].([]*model.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListBuiltInCommands indicates an expected call of ListBuiltInCommands. +func (mr *MockAPIMockRecorder) ListBuiltInCommands() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuiltInCommands", reflect.TypeOf((*MockAPI)(nil).ListBuiltInCommands)) +} + +// ListCommands mocks base method. +func (m *MockAPI) ListCommands(arg0 string) ([]*model.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCommands", arg0) + ret0, _ := ret[0].([]*model.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCommands indicates an expected call of ListCommands. +func (mr *MockAPIMockRecorder) ListCommands(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommands", reflect.TypeOf((*MockAPI)(nil).ListCommands), arg0) +} + +// ListCustomCommands mocks base method. +func (m *MockAPI) ListCustomCommands(arg0 string) ([]*model.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCustomCommands", arg0) + ret0, _ := ret[0].([]*model.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCustomCommands indicates an expected call of ListCustomCommands. +func (mr *MockAPIMockRecorder) ListCustomCommands(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCustomCommands", reflect.TypeOf((*MockAPI)(nil).ListCustomCommands), arg0) +} + +// ListPluginCommands mocks base method. +func (m *MockAPI) ListPluginCommands(arg0 string) ([]*model.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPluginCommands", arg0) + ret0, _ := ret[0].([]*model.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPluginCommands indicates an expected call of ListPluginCommands. +func (mr *MockAPIMockRecorder) ListPluginCommands(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPluginCommands", reflect.TypeOf((*MockAPI)(nil).ListPluginCommands), arg0) +} + +// LoadPluginConfiguration mocks base method. +func (m *MockAPI) LoadPluginConfiguration(arg0 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadPluginConfiguration", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// LoadPluginConfiguration indicates an expected call of LoadPluginConfiguration. +func (mr *MockAPIMockRecorder) LoadPluginConfiguration(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadPluginConfiguration", reflect.TypeOf((*MockAPI)(nil).LoadPluginConfiguration), arg0) +} + +// LogDebug mocks base method. +func (m *MockAPI) LogDebug(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "LogDebug", varargs...) +} + +// LogDebug indicates an expected call of LogDebug. +func (mr *MockAPIMockRecorder) LogDebug(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogDebug", reflect.TypeOf((*MockAPI)(nil).LogDebug), varargs...) +} + +// LogError mocks base method. +func (m *MockAPI) LogError(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "LogError", varargs...) +} + +// LogError indicates an expected call of LogError. +func (mr *MockAPIMockRecorder) LogError(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogError", reflect.TypeOf((*MockAPI)(nil).LogError), varargs...) +} + +// LogInfo mocks base method. +func (m *MockAPI) LogInfo(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "LogInfo", varargs...) +} + +// LogInfo indicates an expected call of LogInfo. +func (mr *MockAPIMockRecorder) LogInfo(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogInfo", reflect.TypeOf((*MockAPI)(nil).LogInfo), varargs...) +} + +// LogWarn mocks base method. +func (m *MockAPI) LogWarn(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "LogWarn", varargs...) +} + +// LogWarn indicates an expected call of LogWarn. +func (mr *MockAPIMockRecorder) LogWarn(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogWarn", reflect.TypeOf((*MockAPI)(nil).LogWarn), varargs...) +} + +// OpenInteractiveDialog mocks base method. +func (m *MockAPI) OpenInteractiveDialog(arg0 model.OpenDialogRequest) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OpenInteractiveDialog", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// OpenInteractiveDialog indicates an expected call of OpenInteractiveDialog. +func (mr *MockAPIMockRecorder) OpenInteractiveDialog(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenInteractiveDialog", reflect.TypeOf((*MockAPI)(nil).OpenInteractiveDialog), arg0) +} + +// PatchBot mocks base method. +func (m *MockAPI) PatchBot(arg0 string, arg1 *model.BotPatch) (*model.Bot, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PatchBot", arg0, arg1) + ret0, _ := ret[0].(*model.Bot) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// PatchBot indicates an expected call of PatchBot. +func (mr *MockAPIMockRecorder) PatchBot(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBot", reflect.TypeOf((*MockAPI)(nil).PatchBot), arg0, arg1) +} + +// PermanentDeleteBot mocks base method. +func (m *MockAPI) PermanentDeleteBot(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PermanentDeleteBot", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// PermanentDeleteBot indicates an expected call of PermanentDeleteBot. +func (mr *MockAPIMockRecorder) PermanentDeleteBot(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentDeleteBot", reflect.TypeOf((*MockAPI)(nil).PermanentDeleteBot), arg0) +} + +// PluginHTTP mocks base method. +func (m *MockAPI) PluginHTTP(arg0 *http.Request) *http.Response { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PluginHTTP", arg0) + ret0, _ := ret[0].(*http.Response) + return ret0 +} + +// PluginHTTP indicates an expected call of PluginHTTP. +func (mr *MockAPIMockRecorder) PluginHTTP(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginHTTP", reflect.TypeOf((*MockAPI)(nil).PluginHTTP), arg0) +} + +// PublishPluginClusterEvent mocks base method. +func (m *MockAPI) PublishPluginClusterEvent(arg0 model.PluginClusterEvent, arg1 model.PluginClusterEventSendOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PublishPluginClusterEvent", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// PublishPluginClusterEvent indicates an expected call of PublishPluginClusterEvent. +func (mr *MockAPIMockRecorder) PublishPluginClusterEvent(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishPluginClusterEvent", reflect.TypeOf((*MockAPI)(nil).PublishPluginClusterEvent), arg0, arg1) +} + +// PublishUserTyping mocks base method. +func (m *MockAPI) PublishUserTyping(arg0, arg1, arg2 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PublishUserTyping", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// PublishUserTyping indicates an expected call of PublishUserTyping. +func (mr *MockAPIMockRecorder) PublishUserTyping(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishUserTyping", reflect.TypeOf((*MockAPI)(nil).PublishUserTyping), arg0, arg1, arg2) +} + +// PublishWebSocketEvent mocks base method. +func (m *MockAPI) PublishWebSocketEvent(arg0 string, arg1 map[string]interface{}, arg2 *model.WebsocketBroadcast) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "PublishWebSocketEvent", arg0, arg1, arg2) +} + +// PublishWebSocketEvent indicates an expected call of PublishWebSocketEvent. +func (mr *MockAPIMockRecorder) PublishWebSocketEvent(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebSocketEvent", reflect.TypeOf((*MockAPI)(nil).PublishWebSocketEvent), arg0, arg1, arg2) +} + +// ReadFile mocks base method. +func (m *MockAPI) ReadFile(arg0 string) ([]byte, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadFile", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// ReadFile indicates an expected call of ReadFile. +func (mr *MockAPIMockRecorder) ReadFile(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadFile", reflect.TypeOf((*MockAPI)(nil).ReadFile), arg0) +} + +// RegisterCommand mocks base method. +func (m *MockAPI) RegisterCommand(arg0 *model.Command) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RegisterCommand", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// RegisterCommand indicates an expected call of RegisterCommand. +func (mr *MockAPIMockRecorder) RegisterCommand(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterCommand", reflect.TypeOf((*MockAPI)(nil).RegisterCommand), arg0) +} + +// RemovePlugin mocks base method. +func (m *MockAPI) RemovePlugin(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemovePlugin", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// RemovePlugin indicates an expected call of RemovePlugin. +func (mr *MockAPIMockRecorder) RemovePlugin(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePlugin", reflect.TypeOf((*MockAPI)(nil).RemovePlugin), arg0) +} + +// RemoveReaction mocks base method. +func (m *MockAPI) RemoveReaction(arg0 *model.Reaction) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveReaction", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// RemoveReaction indicates an expected call of RemoveReaction. +func (mr *MockAPIMockRecorder) RemoveReaction(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveReaction", reflect.TypeOf((*MockAPI)(nil).RemoveReaction), arg0) +} + +// RemoveTeamIcon mocks base method. +func (m *MockAPI) RemoveTeamIcon(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveTeamIcon", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// RemoveTeamIcon indicates an expected call of RemoveTeamIcon. +func (mr *MockAPIMockRecorder) RemoveTeamIcon(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTeamIcon", reflect.TypeOf((*MockAPI)(nil).RemoveTeamIcon), arg0) +} + +// RequestTrialLicense mocks base method. +func (m *MockAPI) RequestTrialLicense(arg0 string, arg1 int, arg2, arg3 bool) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RequestTrialLicense", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// RequestTrialLicense indicates an expected call of RequestTrialLicense. +func (mr *MockAPIMockRecorder) RequestTrialLicense(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestTrialLicense", reflect.TypeOf((*MockAPI)(nil).RequestTrialLicense), arg0, arg1, arg2, arg3) +} + +// RevokeUserAccessToken mocks base method. +func (m *MockAPI) RevokeUserAccessToken(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevokeUserAccessToken", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// RevokeUserAccessToken indicates an expected call of RevokeUserAccessToken. +func (mr *MockAPIMockRecorder) RevokeUserAccessToken(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserAccessToken", reflect.TypeOf((*MockAPI)(nil).RevokeUserAccessToken), arg0) +} + +// SaveConfig mocks base method. +func (m *MockAPI) SaveConfig(arg0 *model.Config) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveConfig", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// SaveConfig indicates an expected call of SaveConfig. +func (mr *MockAPIMockRecorder) SaveConfig(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveConfig", reflect.TypeOf((*MockAPI)(nil).SaveConfig), arg0) +} + +// SavePluginConfig mocks base method. +func (m *MockAPI) SavePluginConfig(arg0 map[string]interface{}) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SavePluginConfig", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// SavePluginConfig indicates an expected call of SavePluginConfig. +func (mr *MockAPIMockRecorder) SavePluginConfig(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePluginConfig", reflect.TypeOf((*MockAPI)(nil).SavePluginConfig), arg0) +} + +// SearchChannels mocks base method. +func (m *MockAPI) SearchChannels(arg0, arg1 string) ([]*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchChannels", arg0, arg1) + ret0, _ := ret[0].([]*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// SearchChannels indicates an expected call of SearchChannels. +func (mr *MockAPIMockRecorder) SearchChannels(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchChannels", reflect.TypeOf((*MockAPI)(nil).SearchChannels), arg0, arg1) +} + +// SearchPostsInTeam mocks base method. +func (m *MockAPI) SearchPostsInTeam(arg0 string, arg1 []*model.SearchParams) ([]*model.Post, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchPostsInTeam", arg0, arg1) + ret0, _ := ret[0].([]*model.Post) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// SearchPostsInTeam indicates an expected call of SearchPostsInTeam. +func (mr *MockAPIMockRecorder) SearchPostsInTeam(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchPostsInTeam", reflect.TypeOf((*MockAPI)(nil).SearchPostsInTeam), arg0, arg1) +} + +// SearchPostsInTeamForUser mocks base method. +func (m *MockAPI) SearchPostsInTeamForUser(arg0, arg1 string, arg2 model.SearchParameter) (*model.PostSearchResults, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchPostsInTeamForUser", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.PostSearchResults) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// SearchPostsInTeamForUser indicates an expected call of SearchPostsInTeamForUser. +func (mr *MockAPIMockRecorder) SearchPostsInTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchPostsInTeamForUser", reflect.TypeOf((*MockAPI)(nil).SearchPostsInTeamForUser), arg0, arg1, arg2) +} + +// SearchTeams mocks base method. +func (m *MockAPI) SearchTeams(arg0 string) ([]*model.Team, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchTeams", arg0) + ret0, _ := ret[0].([]*model.Team) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// SearchTeams indicates an expected call of SearchTeams. +func (mr *MockAPIMockRecorder) SearchTeams(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchTeams", reflect.TypeOf((*MockAPI)(nil).SearchTeams), arg0) +} + +// SearchUsers mocks base method. +func (m *MockAPI) SearchUsers(arg0 *model.UserSearch) ([]*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchUsers", arg0) + ret0, _ := ret[0].([]*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// SearchUsers indicates an expected call of SearchUsers. +func (mr *MockAPIMockRecorder) SearchUsers(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsers", reflect.TypeOf((*MockAPI)(nil).SearchUsers), arg0) +} + +// SendEphemeralPost mocks base method. +func (m *MockAPI) SendEphemeralPost(arg0 string, arg1 *model.Post) *model.Post { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendEphemeralPost", arg0, arg1) + ret0, _ := ret[0].(*model.Post) + return ret0 +} + +// SendEphemeralPost indicates an expected call of SendEphemeralPost. +func (mr *MockAPIMockRecorder) SendEphemeralPost(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEphemeralPost", reflect.TypeOf((*MockAPI)(nil).SendEphemeralPost), arg0, arg1) +} + +// SendMail mocks base method. +func (m *MockAPI) SendMail(arg0, arg1, arg2 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendMail", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// SendMail indicates an expected call of SendMail. +func (mr *MockAPIMockRecorder) SendMail(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMail", reflect.TypeOf((*MockAPI)(nil).SendMail), arg0, arg1, arg2) +} + +// SetProfileImage mocks base method. +func (m *MockAPI) SetProfileImage(arg0 string, arg1 []byte) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetProfileImage", arg0, arg1) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// SetProfileImage indicates an expected call of SetProfileImage. +func (mr *MockAPIMockRecorder) SetProfileImage(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProfileImage", reflect.TypeOf((*MockAPI)(nil).SetProfileImage), arg0, arg1) +} + +// SetTeamIcon mocks base method. +func (m *MockAPI) SetTeamIcon(arg0 string, arg1 []byte) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetTeamIcon", arg0, arg1) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// SetTeamIcon indicates an expected call of SetTeamIcon. +func (mr *MockAPIMockRecorder) SetTeamIcon(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTeamIcon", reflect.TypeOf((*MockAPI)(nil).SetTeamIcon), arg0, arg1) +} + +// SetUserStatusTimedDND mocks base method. +func (m *MockAPI) SetUserStatusTimedDND(arg0 string, arg1 int64) (*model.Status, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUserStatusTimedDND", arg0, arg1) + ret0, _ := ret[0].(*model.Status) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// SetUserStatusTimedDND indicates an expected call of SetUserStatusTimedDND. +func (mr *MockAPIMockRecorder) SetUserStatusTimedDND(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserStatusTimedDND", reflect.TypeOf((*MockAPI)(nil).SetUserStatusTimedDND), arg0, arg1) +} + +// UnregisterCommand mocks base method. +func (m *MockAPI) UnregisterCommand(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnregisterCommand", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnregisterCommand indicates an expected call of UnregisterCommand. +func (mr *MockAPIMockRecorder) UnregisterCommand(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnregisterCommand", reflect.TypeOf((*MockAPI)(nil).UnregisterCommand), arg0, arg1) +} + +// UpdateBotActive mocks base method. +func (m *MockAPI) UpdateBotActive(arg0 string, arg1 bool) (*model.Bot, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateBotActive", arg0, arg1) + ret0, _ := ret[0].(*model.Bot) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateBotActive indicates an expected call of UpdateBotActive. +func (mr *MockAPIMockRecorder) UpdateBotActive(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBotActive", reflect.TypeOf((*MockAPI)(nil).UpdateBotActive), arg0, arg1) +} + +// UpdateChannel mocks base method. +func (m *MockAPI) UpdateChannel(arg0 *model.Channel) (*model.Channel, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateChannel", arg0) + ret0, _ := ret[0].(*model.Channel) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateChannel indicates an expected call of UpdateChannel. +func (mr *MockAPIMockRecorder) UpdateChannel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannel", reflect.TypeOf((*MockAPI)(nil).UpdateChannel), arg0) +} + +// UpdateChannelMemberNotifications mocks base method. +func (m *MockAPI) UpdateChannelMemberNotifications(arg0, arg1 string, arg2 map[string]string) (*model.ChannelMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateChannelMemberNotifications", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.ChannelMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateChannelMemberNotifications indicates an expected call of UpdateChannelMemberNotifications. +func (mr *MockAPIMockRecorder) UpdateChannelMemberNotifications(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelMemberNotifications", reflect.TypeOf((*MockAPI)(nil).UpdateChannelMemberNotifications), arg0, arg1, arg2) +} + +// UpdateChannelMemberRoles mocks base method. +func (m *MockAPI) UpdateChannelMemberRoles(arg0, arg1, arg2 string) (*model.ChannelMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateChannelMemberRoles", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.ChannelMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateChannelMemberRoles indicates an expected call of UpdateChannelMemberRoles. +func (mr *MockAPIMockRecorder) UpdateChannelMemberRoles(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelMemberRoles", reflect.TypeOf((*MockAPI)(nil).UpdateChannelMemberRoles), arg0, arg1, arg2) +} + +// UpdateChannelSidebarCategories mocks base method. +func (m *MockAPI) UpdateChannelSidebarCategories(arg0, arg1 string, arg2 []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateChannelSidebarCategories", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.SidebarCategoryWithChannels) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateChannelSidebarCategories indicates an expected call of UpdateChannelSidebarCategories. +func (mr *MockAPIMockRecorder) UpdateChannelSidebarCategories(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelSidebarCategories", reflect.TypeOf((*MockAPI)(nil).UpdateChannelSidebarCategories), arg0, arg1, arg2) +} + +// UpdateCommand mocks base method. +func (m *MockAPI) UpdateCommand(arg0 string, arg1 *model.Command) (*model.Command, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateCommand", arg0, arg1) + ret0, _ := ret[0].(*model.Command) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateCommand indicates an expected call of UpdateCommand. +func (mr *MockAPIMockRecorder) UpdateCommand(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCommand", reflect.TypeOf((*MockAPI)(nil).UpdateCommand), arg0, arg1) +} + +// UpdateEphemeralPost mocks base method. +func (m *MockAPI) UpdateEphemeralPost(arg0 string, arg1 *model.Post) *model.Post { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateEphemeralPost", arg0, arg1) + ret0, _ := ret[0].(*model.Post) + return ret0 +} + +// UpdateEphemeralPost indicates an expected call of UpdateEphemeralPost. +func (mr *MockAPIMockRecorder) UpdateEphemeralPost(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEphemeralPost", reflect.TypeOf((*MockAPI)(nil).UpdateEphemeralPost), arg0, arg1) +} + +// UpdateOAuthApp mocks base method. +func (m *MockAPI) UpdateOAuthApp(arg0 *model.OAuthApp) (*model.OAuthApp, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateOAuthApp", arg0) + ret0, _ := ret[0].(*model.OAuthApp) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateOAuthApp indicates an expected call of UpdateOAuthApp. +func (mr *MockAPIMockRecorder) UpdateOAuthApp(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuthApp", reflect.TypeOf((*MockAPI)(nil).UpdateOAuthApp), arg0) +} + +// UpdatePost mocks base method. +func (m *MockAPI) UpdatePost(arg0 *model.Post) (*model.Post, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePost", arg0) + ret0, _ := ret[0].(*model.Post) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdatePost indicates an expected call of UpdatePost. +func (mr *MockAPIMockRecorder) UpdatePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePost", reflect.TypeOf((*MockAPI)(nil).UpdatePost), arg0) +} + +// UpdatePreferencesForUser mocks base method. +func (m *MockAPI) UpdatePreferencesForUser(arg0 string, arg1 []model.Preference) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePreferencesForUser", arg0, arg1) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// UpdatePreferencesForUser indicates an expected call of UpdatePreferencesForUser. +func (mr *MockAPIMockRecorder) UpdatePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferencesForUser", reflect.TypeOf((*MockAPI)(nil).UpdatePreferencesForUser), arg0, arg1) +} + +// UpdateTeam mocks base method. +func (m *MockAPI) UpdateTeam(arg0 *model.Team) (*model.Team, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTeam", arg0) + ret0, _ := ret[0].(*model.Team) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateTeam indicates an expected call of UpdateTeam. +func (mr *MockAPIMockRecorder) UpdateTeam(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTeam", reflect.TypeOf((*MockAPI)(nil).UpdateTeam), arg0) +} + +// UpdateTeamMemberRoles mocks base method. +func (m *MockAPI) UpdateTeamMemberRoles(arg0, arg1, arg2 string) (*model.TeamMember, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTeamMemberRoles", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.TeamMember) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateTeamMemberRoles indicates an expected call of UpdateTeamMemberRoles. +func (mr *MockAPIMockRecorder) UpdateTeamMemberRoles(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTeamMemberRoles", reflect.TypeOf((*MockAPI)(nil).UpdateTeamMemberRoles), arg0, arg1, arg2) +} + +// UpdateUser mocks base method. +func (m *MockAPI) UpdateUser(arg0 *model.User) (*model.User, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUser", arg0) + ret0, _ := ret[0].(*model.User) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateUser indicates an expected call of UpdateUser. +func (mr *MockAPIMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockAPI)(nil).UpdateUser), arg0) +} + +// UpdateUserActive mocks base method. +func (m *MockAPI) UpdateUserActive(arg0 string, arg1 bool) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserActive", arg0, arg1) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// UpdateUserActive indicates an expected call of UpdateUserActive. +func (mr *MockAPIMockRecorder) UpdateUserActive(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserActive", reflect.TypeOf((*MockAPI)(nil).UpdateUserActive), arg0, arg1) +} + +// UpdateUserStatus mocks base method. +func (m *MockAPI) UpdateUserStatus(arg0, arg1 string) (*model.Status, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserStatus", arg0, arg1) + ret0, _ := ret[0].(*model.Status) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateUserStatus indicates an expected call of UpdateUserStatus. +func (mr *MockAPIMockRecorder) UpdateUserStatus(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockAPI)(nil).UpdateUserStatus), arg0, arg1) +} + +// UploadFile mocks base method. +func (m *MockAPI) UploadFile(arg0 []byte, arg1, arg2 string) (*model.FileInfo, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UploadFile", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.FileInfo) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UploadFile indicates an expected call of UploadFile. +func (mr *MockAPIMockRecorder) UploadFile(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadFile", reflect.TypeOf((*MockAPI)(nil).UploadFile), arg0, arg1, arg2) +} diff --git a/server/services/permissions/mocks/mockstore.go b/server/services/permissions/mocks/mockstore.go new file mode 100644 index 000000000..59e8fdb21 --- /dev/null +++ b/server/services/permissions/mocks/mockstore.go @@ -0,0 +1,65 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/focalboard/server/services/permissions (interfaces: Store) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/focalboard/server/model" +) + +// MockStore is a mock of Store interface. +type MockStore struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore +} + +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore) EXPECT() *MockStoreMockRecorder { + return m.recorder +} + +// GetBoard mocks base method. +func (m *MockStore) GetBoard(arg0 string) (*model.Board, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBoard", arg0) + ret0, _ := ret[0].(*model.Board) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBoard indicates an expected call of GetBoard. +func (mr *MockStoreMockRecorder) GetBoard(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoard", reflect.TypeOf((*MockStore)(nil).GetBoard), arg0) +} + +// GetMemberForBoard mocks base method. +func (m *MockStore) GetMemberForBoard(arg0, arg1 string) (*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMemberForBoard", arg0, arg1) + ret0, _ := ret[0].(*model.BoardMember) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMemberForBoard indicates an expected call of GetMemberForBoard. +func (mr *MockStoreMockRecorder) GetMemberForBoard(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMemberForBoard", reflect.TypeOf((*MockStore)(nil).GetMemberForBoard), arg0, arg1) +} diff --git a/server/services/permissions/permissions.go b/server/services/permissions/permissions.go new file mode 100644 index 000000000..67c3416c3 --- /dev/null +++ b/server/services/permissions/permissions.go @@ -0,0 +1,21 @@ +//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package permissions + +import ( + "github.com/mattermost/focalboard/server/model" + + mmModel "github.com/mattermost/mattermost-server/v6/model" +) + +type PermissionsService interface { + HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool + HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool +} + +type Store interface { + GetBoard(boardID string) (*model.Board, error) + GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) +} diff --git a/server/services/store/generators/transactional_store.go.tmpl b/server/services/store/generators/transactional_store.go.tmpl index 13a4909f1..b3d8b668e 100644 --- a/server/services/store/generators/transactional_store.go.tmpl +++ b/server/services/store/generators/transactional_store.go.tmpl @@ -17,7 +17,6 @@ import ( "time" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) @@ -25,7 +24,7 @@ import ( {{range $index, $element := .Methods}} func (s *SQLStore) {{$index}}({{$element.Params | joinParamsWithType}}) {{$element.Results | joinResultsForSignature}} { {{- if $element.WithTransaction}} - if s.dbType == sqliteDBType { + if s.dbType == model.SqliteDBType { return s.{{$index | renameStoreMethod}}(s.db, {{$element.Params | joinParams}}) } tx, txErr := s.db.BeginTx(context.Background(), nil) diff --git a/server/services/store/mattermostauthlayer/mattermostauthlayer.go b/server/services/store/mattermostauthlayer/mattermostauthlayer.go index 12d1a8198..18f8e5be3 100644 --- a/server/services/store/mattermostauthlayer/mattermostauthlayer.go +++ b/server/services/store/mattermostauthlayer/mattermostauthlayer.go @@ -3,13 +3,9 @@ package mattermostauthlayer import ( "database/sql" "encoding/json" - "fmt" - "strings" "github.com/mattermost/mattermost-server/v6/plugin" - "github.com/pkg/errors" - sq "github.com/Masterminds/squirrel" "github.com/mattermost/focalboard/server/model" @@ -19,18 +15,6 @@ import ( "github.com/mattermost/mattermost-server/v6/shared/mlog" ) -const ( - sqliteDBType = "sqlite3" - postgresDBType = "postgres" - mysqlDBType = "mysql" - - directChannelType = "D" -) - -var ( - errUnsupportedDatabaseError = errors.New("method is unsupported on current database. Supported databases are - MySQL and PostgreSQL") -) - type NotSupportedError struct { msg string } @@ -99,10 +83,11 @@ func (s *MattermostAuthLayer) getUserByCondition(condition sq.Eq) (*model.User, func (s *MattermostAuthLayer) getUsersByCondition(condition sq.Eq) (map[string]*model.User, error) { query := s.getQueryBuilder(). - Select("id", "username", "email", "password", "MFASecret as mfa_secret", "AuthService as auth_service", "COALESCE(AuthData, '') as auth_data", - "props", "CreateAt as create_at", "UpdateAt as update_at", "DeleteAt as delete_at"). - From("Users"). - Where(sq.Eq{"deleteAt": 0}). + Select("u.id", "u.username", "u.email", "u.password", "u.MFASecret as mfa_secret", "u.AuthService as auth_service", "COALESCE(u.AuthData, '') as auth_data", + "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at", "u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot"). + From("Users as u"). + LeftJoin("Bots b ON ( b.UserId = u.ID )"). + Where(sq.Eq{"u.deleteAt": 0}). Where(condition) row, err := query.Query() if err != nil { @@ -116,7 +101,7 @@ func (s *MattermostAuthLayer) getUsersByCondition(condition sq.Eq) (map[string]* var propsBytes []byte err := row.Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.MfaSecret, &user.AuthService, - &user.AuthData, &propsBytes, &user.CreateAt, &user.UpdateAt, &user.DeleteAt) + &user.AuthData, &propsBytes, &user.CreateAt, &user.UpdateAt, &user.DeleteAt, &user.IsBot) if err != nil { return nil, err } @@ -229,98 +214,114 @@ func (s *MattermostAuthLayer) CleanUpSessions(expireTime int64) error { return NotSupportedError{"no update allowed from focalboard, update it using mattermost"} } -func (s *MattermostAuthLayer) GetWorkspace(id string) (*model.Workspace, error) { +func (s *MattermostAuthLayer) GetTeam(id string) (*model.Team, error) { if id == "0" { - workspace := model.Workspace{ + team := model.Team{ ID: id, Title: "", } - return &workspace, nil + return &team, nil } query := s.getQueryBuilder(). - Select("DisplayName, Type"). - From("Channels"). + Select("DisplayName"). + From("Teams"). Where(sq.Eq{"ID": id}) row := query.QueryRow() var displayName string - var channelType string - err := row.Scan(&displayName, &channelType) + err := row.Scan(&displayName) if err != nil { + s.logger.Error("GetTeam scan error", mlog.Err(err)) return nil, err } - if channelType != "D" && channelType != "G" { - return &model.Workspace{ID: id, Title: displayName}, nil - } + return &model.Team{ID: id, Title: displayName}, nil +} - query = s.getQueryBuilder(). - Select("Username"). - From("ChannelMembers"). - Join("Users ON Users.ID=ChannelMembers.UserID"). - Where(sq.Eq{"ChannelID": id}) +// GetTeamsForUser retrieves all the teams that the user is a member of. +func (s *MattermostAuthLayer) GetTeamsForUser(userID string) ([]*model.Team, error) { + query := s.getQueryBuilder(). + Select("t.Id", "t.DisplayName"). + From("Teams as t"). + Join("TeamMembers as tm on t.Id=tm.TeamId"). + Where(sq.Eq{"tm.UserId": userID}) - var sb strings.Builder rows, err := query.Query() if err != nil { return nil, err } defer s.CloseRows(rows) - first := true + teams := []*model.Team{} for rows.Next() { - if first { - first = false - } else { - sb.WriteString(", ") - } - var name string - if err := rows.Scan(&name); err != nil { + var team model.Team + + err := rows.Scan( + &team.ID, + &team.Title, + ) + if err != nil { return nil, err } - sb.WriteString(name) - } - return &model.Workspace{ID: id, Title: sb.String()}, nil -} -func (s *MattermostAuthLayer) HasWorkspaceAccess(userID string, workspaceID string) (bool, error) { - query := s.getQueryBuilder(). - Select("count(*)"). - From("ChannelMembers"). - Where(sq.Eq{"ChannelID": workspaceID}). - Where(sq.Eq{"UserID": userID}) - - row := query.QueryRow() - - var count int - err := row.Scan(&count) - if err != nil { - return false, err + teams = append(teams, &team) } - return count > 0, nil + return teams, nil } func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType { builder := sq.StatementBuilder - if s.dbType == postgresDBType || s.dbType == sqliteDBType { + if s.dbType == model.PostgresDBType || s.dbType == model.SqliteDBType { builder = builder.PlaceholderFormat(sq.Dollar) } return builder.RunWith(s.mmDB) } -func (s *MattermostAuthLayer) GetUsersByWorkspace(workspaceID string) ([]*model.User, error) { +func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, error) { query := s.getQueryBuilder(). - Select("id", "username", "props", - "Users.CreateAt as create_at", "Users.UpdateAt as update_at", "Users.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot"). - From("Users"). - Join("ChannelMembers ON ChannelMembers.UserID = Users.ID"). + Select("u.id", "u.username", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at", + "u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot"). + From("Users as u"). + Join("TeamMembers as tm ON tm.UserID = u.ID"). LeftJoin("Bots b ON ( b.UserId = Users.ID )"). - Where(sq.Eq{"Users.deleteAt": 0}). - Where(sq.Eq{"ChannelMembers.ChannelId": workspaceID}) + Where(sq.Eq{"u.deleteAt": 0}). + Where(sq.Eq{"tm.TeamId": teamID}) + + rows, err := query.Query() + if err != nil { + return nil, err + } + defer s.CloseRows(rows) + + users, err := s.usersFromRows(rows) + if err != nil { + return nil, err + } + + return users, nil +} + +func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) { + query := s.getQueryBuilder(). + Select("u.id", "u.username", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at", + "u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot"). + From("Users as u"). + Join("TeamMembers as tm ON tm.UserID = u.id"). + LeftJoin("Bots b ON ( b.UserId = u.id )"). + Where(sq.Eq{"u.deleteAt": 0}). + Where(sq.Or{ + sq.Like{"u.username": "%" + searchQuery + "%"}, + sq.Like{"u.nickname": "%" + searchQuery + "%"}, + sq.Like{"u.firstname": "%" + searchQuery + "%"}, + sq.Like{"u.lastname": "%" + searchQuery + "%"}, + }). + Where(sq.Eq{"tm.TeamId": teamID}). + OrderBy("u.username"). + Limit(10) rows, err := query.Query() if err != nil { @@ -373,122 +374,6 @@ func (s *MattermostAuthLayer) CloseRows(rows *sql.Rows) { } } -func (s *MattermostAuthLayer) GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) { - var query sq.SelectBuilder - - var nonTemplateFilter string - - switch s.dbType { - case mysqlDBType: - nonTemplateFilter = "focalboard_blocks.fields LIKE '%\"isTemplate\":false%'" - case postgresDBType: - nonTemplateFilter = "focalboard_blocks.fields ->> 'isTemplate' = 'false'" - default: - return nil, fmt.Errorf("GetUserWorkspaces - %w", errUnsupportedDatabaseError) - } - - query = s.getQueryBuilder(). - Select("Channels.ID", "Channels.DisplayName", "COUNT(focalboard_blocks.id), Channels.Type, Channels.Name"). - From("ChannelMembers"). - // select channels without a corresponding workspace - LeftJoin( - "focalboard_blocks ON focalboard_blocks.workspace_id = ChannelMembers.ChannelId AND "+ - "focalboard_blocks.type = 'board' AND "+ - nonTemplateFilter, - ). - Join("Channels ON ChannelMembers.ChannelId = Channels.Id"). - Where(sq.Eq{"ChannelMembers.UserId": userID}). - GroupBy("Channels.Id", "Channels.DisplayName") - - rows, err := query.Query() - if err != nil { - s.logger.Error("ERROR GetUserWorkspaces", mlog.Err(err)) - return nil, err - } - - defer s.CloseRows(rows) - return s.userWorkspacesFromRows(rows) -} - -type UserWorkspaceRawModel struct { - ID string `json:"id"` - Title string `json:"title"` - BoardCount int `json:"boardCount"` - Type string `json:"type"` - Name string `json:"name"` -} - -func (s *MattermostAuthLayer) userWorkspacesFromRows(rows *sql.Rows) ([]model.UserWorkspace, error) { - rawUserWorkspaces := []UserWorkspaceRawModel{} - usersToFetch := []string{} - - for rows.Next() { - var rawUserWorkspace UserWorkspaceRawModel - - err := rows.Scan( - &rawUserWorkspace.ID, - &rawUserWorkspace.Title, - &rawUserWorkspace.BoardCount, - &rawUserWorkspace.Type, - &rawUserWorkspace.Name, - ) - - if err != nil { - s.logger.Error("ERROR userWorkspacesFromRows", mlog.Err(err)) - return nil, err - } - - if rawUserWorkspace.Type == directChannelType { - userIDs := strings.Split(rawUserWorkspace.Name, "__") - usersToFetch = append(usersToFetch, userIDs...) - } - - rawUserWorkspaces = append(rawUserWorkspaces, rawUserWorkspace) - } - - var users map[string]*model.User - - if len(usersToFetch) > 0 { - var err error - users, err = s.getUsersByCondition(sq.Eq{"id": usersToFetch}) - if err != nil { - return nil, err - } - } - - userWorkspaces := []model.UserWorkspace{} - - for i := range rawUserWorkspaces { - if rawUserWorkspaces[i].Type == directChannelType { - userIDs := strings.Split(rawUserWorkspaces[i].Name, "__") - names := []string{} - - for _, userID := range userIDs { - user, exists := users[userID] - // Shows "(Deleted User)" instead of long, unreadable UUID in case the user is not found - username := "(Deleted User)" - if exists { - username = user.Username - } - - names = append(names, username) - } - - rawUserWorkspaces[i].Title = strings.Join(names, ", ") - } - - userWorkspace := model.UserWorkspace{ - ID: rawUserWorkspaces[i].ID, - Title: rawUserWorkspaces[i].Title, - BoardCount: rawUserWorkspaces[i].BoardCount, - } - - userWorkspaces = append(userWorkspaces, userWorkspace) - } - - return userWorkspaces, nil -} - func (s *MattermostAuthLayer) CreatePrivateWorkspace(userID string) (string, error) { // we emulate a private workspace by creating // a DM channel from the user to themselves. diff --git a/server/services/store/mockstore/mockstore.go b/server/services/store/mockstore/mockstore.go index 51a21c565..68dc50af3 100644 --- a/server/services/store/mockstore/mockstore.go +++ b/server/services/store/mockstore/mockstore.go @@ -10,7 +10,6 @@ import ( gomock "github.com/golang/mock/gomock" model "github.com/mattermost/focalboard/server/model" - store "github.com/mattermost/focalboard/server/services/store" ) // MockStore is a mock of Store interface. @@ -36,6 +35,20 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder { return m.recorder } +// AddUpdateCategoryBlock mocks base method. +func (m *MockStore) AddUpdateCategoryBlock(arg0, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddUpdateCategoryBlock", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddUpdateCategoryBlock indicates an expected call of AddUpdateCategoryBlock. +func (mr *MockStoreMockRecorder) AddUpdateCategoryBlock(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBlock", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBlock), arg0, arg1, arg2) +} + // CleanUpSessions mocks base method. func (m *MockStore) CleanUpSessions(arg0 int64) error { m.ctrl.T.Helper() @@ -50,19 +63,49 @@ func (mr *MockStoreMockRecorder) CleanUpSessions(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpSessions", reflect.TypeOf((*MockStore)(nil).CleanUpSessions), arg0) } -// CreatePrivateWorkspace mocks base method. -func (m *MockStore) CreatePrivateWorkspace(arg0 string) (string, error) { +// CreateBoardsAndBlocks mocks base method. +func (m *MockStore) CreateBoardsAndBlocks(arg0 *model.BoardsAndBlocks, arg1 string) (*model.BoardsAndBlocks, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePrivateWorkspace", arg0) - ret0, _ := ret[0].(string) + ret := m.ctrl.Call(m, "CreateBoardsAndBlocks", arg0, arg1) + ret0, _ := ret[0].(*model.BoardsAndBlocks) ret1, _ := ret[1].(error) return ret0, ret1 } -// CreatePrivateWorkspace indicates an expected call of CreatePrivateWorkspace. -func (mr *MockStoreMockRecorder) CreatePrivateWorkspace(arg0 interface{}) *gomock.Call { +// CreateBoardsAndBlocks indicates an expected call of CreateBoardsAndBlocks. +func (mr *MockStoreMockRecorder) CreateBoardsAndBlocks(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePrivateWorkspace", reflect.TypeOf((*MockStore)(nil).CreatePrivateWorkspace), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBoardsAndBlocks", reflect.TypeOf((*MockStore)(nil).CreateBoardsAndBlocks), arg0, arg1) +} + +// CreateBoardsAndBlocksWithAdmin mocks base method. +func (m *MockStore) CreateBoardsAndBlocksWithAdmin(arg0 *model.BoardsAndBlocks, arg1 string) (*model.BoardsAndBlocks, []*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateBoardsAndBlocksWithAdmin", arg0, arg1) + ret0, _ := ret[0].(*model.BoardsAndBlocks) + ret1, _ := ret[1].([]*model.BoardMember) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateBoardsAndBlocksWithAdmin indicates an expected call of CreateBoardsAndBlocksWithAdmin. +func (mr *MockStoreMockRecorder) CreateBoardsAndBlocksWithAdmin(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBoardsAndBlocksWithAdmin", reflect.TypeOf((*MockStore)(nil).CreateBoardsAndBlocksWithAdmin), arg0, arg1) +} + +// CreateCategory mocks base method. +func (m *MockStore) CreateCategory(arg0 model.Category) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCategory", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateCategory indicates an expected call of CreateCategory. +func (mr *MockStoreMockRecorder) CreateCategory(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCategory", reflect.TypeOf((*MockStore)(nil).CreateCategory), arg0) } // CreateSession mocks base method. @@ -80,18 +123,18 @@ func (mr *MockStoreMockRecorder) CreateSession(arg0 interface{}) *gomock.Call { } // CreateSubscription mocks base method. -func (m *MockStore) CreateSubscription(arg0 store.Container, arg1 *model.Subscription) (*model.Subscription, error) { +func (m *MockStore) CreateSubscription(arg0 *model.Subscription) (*model.Subscription, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSubscription", arg0, arg1) + ret := m.ctrl.Call(m, "CreateSubscription", arg0) ret0, _ := ret[0].(*model.Subscription) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateSubscription indicates an expected call of CreateSubscription. -func (mr *MockStoreMockRecorder) CreateSubscription(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateSubscription(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscription", reflect.TypeOf((*MockStore)(nil).CreateSubscription), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscription", reflect.TypeOf((*MockStore)(nil).CreateSubscription), arg0) } // CreateUser mocks base method. @@ -123,31 +166,87 @@ func (mr *MockStoreMockRecorder) DBType() *gomock.Call { } // DeleteBlock mocks base method. -func (m *MockStore) DeleteBlock(arg0 store.Container, arg1, arg2 string) error { +func (m *MockStore) DeleteBlock(arg0, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteBlock", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "DeleteBlock", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DeleteBlock indicates an expected call of DeleteBlock. -func (mr *MockStoreMockRecorder) DeleteBlock(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteBlock(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlock", reflect.TypeOf((*MockStore)(nil).DeleteBlock), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlock", reflect.TypeOf((*MockStore)(nil).DeleteBlock), arg0, arg1) +} + +// DeleteBoard mocks base method. +func (m *MockStore) DeleteBoard(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteBoard", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteBoard indicates an expected call of DeleteBoard. +func (mr *MockStoreMockRecorder) DeleteBoard(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoard", reflect.TypeOf((*MockStore)(nil).DeleteBoard), arg0, arg1) +} + +// DeleteBoardsAndBlocks mocks base method. +func (m *MockStore) DeleteBoardsAndBlocks(arg0 *model.DeleteBoardsAndBlocks, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteBoardsAndBlocks", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteBoardsAndBlocks indicates an expected call of DeleteBoardsAndBlocks. +func (mr *MockStoreMockRecorder) DeleteBoardsAndBlocks(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoardsAndBlocks", reflect.TypeOf((*MockStore)(nil).DeleteBoardsAndBlocks), arg0, arg1) +} + +// DeleteCategory mocks base method. +func (m *MockStore) DeleteCategory(arg0, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCategory", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCategory indicates an expected call of DeleteCategory. +func (mr *MockStoreMockRecorder) DeleteCategory(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCategory", reflect.TypeOf((*MockStore)(nil).DeleteCategory), arg0, arg1, arg2) +} + +// DeleteMember mocks base method. +func (m *MockStore) DeleteMember(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMember", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMember indicates an expected call of DeleteMember. +func (mr *MockStoreMockRecorder) DeleteMember(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMember", reflect.TypeOf((*MockStore)(nil).DeleteMember), arg0, arg1) } // DeleteNotificationHint mocks base method. -func (m *MockStore) DeleteNotificationHint(arg0 store.Container, arg1 string) error { +func (m *MockStore) DeleteNotificationHint(arg0 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteNotificationHint", arg0, arg1) + ret := m.ctrl.Call(m, "DeleteNotificationHint", arg0) ret0, _ := ret[0].(error) return ret0 } // DeleteNotificationHint indicates an expected call of DeleteNotificationHint. -func (mr *MockStoreMockRecorder) DeleteNotificationHint(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteNotificationHint(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNotificationHint", reflect.TypeOf((*MockStore)(nil).DeleteNotificationHint), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNotificationHint", reflect.TypeOf((*MockStore)(nil).DeleteNotificationHint), arg0) } // DeleteSession mocks base method. @@ -165,17 +264,48 @@ func (mr *MockStoreMockRecorder) DeleteSession(arg0 interface{}) *gomock.Call { } // DeleteSubscription mocks base method. -func (m *MockStore) DeleteSubscription(arg0 store.Container, arg1, arg2 string) error { +func (m *MockStore) DeleteSubscription(arg0, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSubscription", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "DeleteSubscription", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DeleteSubscription indicates an expected call of DeleteSubscription. -func (mr *MockStoreMockRecorder) DeleteSubscription(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteSubscription(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubscription", reflect.TypeOf((*MockStore)(nil).DeleteSubscription), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubscription", reflect.TypeOf((*MockStore)(nil).DeleteSubscription), arg0, arg1) +} + +// DuplicateBlock mocks base method. +func (m *MockStore) DuplicateBlock(arg0, arg1, arg2 string, arg3 bool) ([]model.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DuplicateBlock", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]model.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DuplicateBlock indicates an expected call of DuplicateBlock. +func (mr *MockStoreMockRecorder) DuplicateBlock(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DuplicateBlock", reflect.TypeOf((*MockStore)(nil).DuplicateBlock), arg0, arg1, arg2, arg3) +} + +// DuplicateBoard mocks base method. +func (m *MockStore) DuplicateBoard(arg0, arg1, arg2 string, arg3 bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DuplicateBoard", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*model.BoardsAndBlocks) + ret1, _ := ret[1].([]*model.BoardMember) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// DuplicateBoard indicates an expected call of DuplicateBoard. +func (mr *MockStoreMockRecorder) DuplicateBoard(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DuplicateBoard", reflect.TypeOf((*MockStore)(nil).DuplicateBoard), arg0, arg1, arg2, arg3) } // GetActiveUserCount mocks base method. @@ -193,34 +323,34 @@ func (mr *MockStoreMockRecorder) GetActiveUserCount(arg0 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), arg0) } -// GetAllBlocks mocks base method. -func (m *MockStore) GetAllBlocks(arg0 store.Container) ([]model.Block, error) { +// GetAllTeams mocks base method. +func (m *MockStore) GetAllTeams() ([]*model.Team, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAllBlocks", arg0) - ret0, _ := ret[0].([]model.Block) + ret := m.ctrl.Call(m, "GetAllTeams") + ret0, _ := ret[0].([]*model.Team) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetAllBlocks indicates an expected call of GetAllBlocks. -func (mr *MockStoreMockRecorder) GetAllBlocks(arg0 interface{}) *gomock.Call { +// GetAllTeams indicates an expected call of GetAllTeams. +func (mr *MockStoreMockRecorder) GetAllTeams() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBlocks", reflect.TypeOf((*MockStore)(nil).GetAllBlocks), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTeams", reflect.TypeOf((*MockStore)(nil).GetAllTeams)) } // GetBlock mocks base method. -func (m *MockStore) GetBlock(arg0 store.Container, arg1 string) (*model.Block, error) { +func (m *MockStore) GetBlock(arg0 string) (*model.Block, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlock", arg0, arg1) + ret := m.ctrl.Call(m, "GetBlock", arg0) ret0, _ := ret[0].(*model.Block) ret1, _ := ret[1].(error) return ret0, ret1 } // GetBlock indicates an expected call of GetBlock. -func (mr *MockStoreMockRecorder) GetBlock(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetBlock(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockStore)(nil).GetBlock), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockStore)(nil).GetBlock), arg0) } // GetBlockCountsByType mocks base method. @@ -239,22 +369,52 @@ func (mr *MockStoreMockRecorder) GetBlockCountsByType() *gomock.Call { } // GetBlockHistory mocks base method. -func (m *MockStore) GetBlockHistory(arg0 store.Container, arg1 string, arg2 model.QueryBlockHistoryOptions) ([]model.Block, error) { +func (m *MockStore) GetBlockHistory(arg0 string, arg1 model.QueryBlockHistoryOptions) ([]model.Block, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlockHistory", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetBlockHistory", arg0, arg1) ret0, _ := ret[0].([]model.Block) ret1, _ := ret[1].(error) return ret0, ret1 } // GetBlockHistory indicates an expected call of GetBlockHistory. -func (mr *MockStoreMockRecorder) GetBlockHistory(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetBlockHistory(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistory", reflect.TypeOf((*MockStore)(nil).GetBlockHistory), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistory", reflect.TypeOf((*MockStore)(nil).GetBlockHistory), arg0, arg1) +} + +// GetBlocksForBoard mocks base method. +func (m *MockStore) GetBlocksForBoard(arg0 string) ([]model.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlocksForBoard", arg0) + ret0, _ := ret[0].([]model.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlocksForBoard indicates an expected call of GetBlocksForBoard. +func (mr *MockStoreMockRecorder) GetBlocksForBoard(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksForBoard", reflect.TypeOf((*MockStore)(nil).GetBlocksForBoard), arg0) +} + +// GetBlocksWithBoardID mocks base method. +func (m *MockStore) GetBlocksWithBoardID(arg0 string) ([]model.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlocksWithBoardID", arg0) + ret0, _ := ret[0].([]model.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlocksWithBoardID indicates an expected call of GetBlocksWithBoardID. +func (mr *MockStoreMockRecorder) GetBlocksWithBoardID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithBoardID", reflect.TypeOf((*MockStore)(nil).GetBlocksWithBoardID), arg0) } // GetBlocksWithParent mocks base method. -func (m *MockStore) GetBlocksWithParent(arg0 store.Container, arg1 string) ([]model.Block, error) { +func (m *MockStore) GetBlocksWithParent(arg0, arg1 string) ([]model.Block, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetBlocksWithParent", arg0, arg1) ret0, _ := ret[0].([]model.Block) @@ -269,7 +429,7 @@ func (mr *MockStoreMockRecorder) GetBlocksWithParent(arg0, arg1 interface{}) *go } // GetBlocksWithParentAndType mocks base method. -func (m *MockStore) GetBlocksWithParentAndType(arg0 store.Container, arg1, arg2 string) ([]model.Block, error) { +func (m *MockStore) GetBlocksWithParentAndType(arg0, arg1, arg2 string) ([]model.Block, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetBlocksWithParentAndType", arg0, arg1, arg2) ret0, _ := ret[0].([]model.Block) @@ -283,23 +443,8 @@ func (mr *MockStoreMockRecorder) GetBlocksWithParentAndType(arg0, arg1, arg2 int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParentAndType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParentAndType), arg0, arg1, arg2) } -// GetBlocksWithRootID mocks base method. -func (m *MockStore) GetBlocksWithRootID(arg0 store.Container, arg1 string) ([]model.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlocksWithRootID", arg0, arg1) - ret0, _ := ret[0].([]model.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlocksWithRootID indicates an expected call of GetBlocksWithRootID. -func (mr *MockStoreMockRecorder) GetBlocksWithRootID(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithRootID", reflect.TypeOf((*MockStore)(nil).GetBlocksWithRootID), arg0, arg1) -} - // GetBlocksWithType mocks base method. -func (m *MockStore) GetBlocksWithType(arg0 store.Container, arg1 string) ([]model.Block, error) { +func (m *MockStore) GetBlocksWithType(arg0, arg1 string) ([]model.Block, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetBlocksWithType", arg0, arg1) ret0, _ := ret[0].([]model.Block) @@ -313,51 +458,126 @@ func (mr *MockStoreMockRecorder) GetBlocksWithType(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithType), arg0, arg1) } -// GetBoardAndCard mocks base method. -func (m *MockStore) GetBoardAndCard(arg0 store.Container, arg1 *model.Block) (*model.Block, *model.Block, error) { +// GetBoard mocks base method. +func (m *MockStore) GetBoard(arg0 string) (*model.Board, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardAndCard", arg0, arg1) - ret0, _ := ret[0].(*model.Block) + ret := m.ctrl.Call(m, "GetBoard", arg0) + ret0, _ := ret[0].(*model.Board) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBoard indicates an expected call of GetBoard. +func (mr *MockStoreMockRecorder) GetBoard(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoard", reflect.TypeOf((*MockStore)(nil).GetBoard), arg0) +} + +// GetBoardAndCard mocks base method. +func (m *MockStore) GetBoardAndCard(arg0 *model.Block) (*model.Board, *model.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBoardAndCard", arg0) + ret0, _ := ret[0].(*model.Board) ret1, _ := ret[1].(*model.Block) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // GetBoardAndCard indicates an expected call of GetBoardAndCard. -func (mr *MockStoreMockRecorder) GetBoardAndCard(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetBoardAndCard(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCard", reflect.TypeOf((*MockStore)(nil).GetBoardAndCard), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCard", reflect.TypeOf((*MockStore)(nil).GetBoardAndCard), arg0) } // GetBoardAndCardByID mocks base method. -func (m *MockStore) GetBoardAndCardByID(arg0 store.Container, arg1 string) (*model.Block, *model.Block, error) { +func (m *MockStore) GetBoardAndCardByID(arg0 string) (*model.Board, *model.Block, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardAndCardByID", arg0, arg1) - ret0, _ := ret[0].(*model.Block) + ret := m.ctrl.Call(m, "GetBoardAndCardByID", arg0) + ret0, _ := ret[0].(*model.Board) ret1, _ := ret[1].(*model.Block) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // GetBoardAndCardByID indicates an expected call of GetBoardAndCardByID. -func (mr *MockStoreMockRecorder) GetBoardAndCardByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetBoardAndCardByID(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCardByID", reflect.TypeOf((*MockStore)(nil).GetBoardAndCardByID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCardByID", reflect.TypeOf((*MockStore)(nil).GetBoardAndCardByID), arg0) } -// GetDefaultTemplateBlocks mocks base method. -func (m *MockStore) GetDefaultTemplateBlocks() ([]model.Block, error) { +// GetBoardsForUserAndTeam mocks base method. +func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string) ([]*model.Board, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDefaultTemplateBlocks") - ret0, _ := ret[0].([]model.Block) + ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1) + ret0, _ := ret[0].([]*model.Board) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetDefaultTemplateBlocks indicates an expected call of GetDefaultTemplateBlocks. -func (mr *MockStoreMockRecorder) GetDefaultTemplateBlocks() *gomock.Call { +// GetBoardsForUserAndTeam indicates an expected call of GetBoardsForUserAndTeam. +func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultTemplateBlocks", reflect.TypeOf((*MockStore)(nil).GetDefaultTemplateBlocks)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1) +} + +// GetCategory mocks base method. +func (m *MockStore) GetCategory(arg0 string) (*model.Category, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCategory", arg0) + ret0, _ := ret[0].(*model.Category) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCategory indicates an expected call of GetCategory. +func (mr *MockStoreMockRecorder) GetCategory(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), arg0) +} + +// GetMemberForBoard mocks base method. +func (m *MockStore) GetMemberForBoard(arg0, arg1 string) (*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMemberForBoard", arg0, arg1) + ret0, _ := ret[0].(*model.BoardMember) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMemberForBoard indicates an expected call of GetMemberForBoard. +func (mr *MockStoreMockRecorder) GetMemberForBoard(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMemberForBoard", reflect.TypeOf((*MockStore)(nil).GetMemberForBoard), arg0, arg1) +} + +// GetMembersForBoard mocks base method. +func (m *MockStore) GetMembersForBoard(arg0 string) ([]*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMembersForBoard", arg0) + ret0, _ := ret[0].([]*model.BoardMember) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMembersForBoard indicates an expected call of GetMembersForBoard. +func (mr *MockStoreMockRecorder) GetMembersForBoard(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembersForBoard", reflect.TypeOf((*MockStore)(nil).GetMembersForBoard), arg0) +} + +// GetMembersForUser mocks base method. +func (m *MockStore) GetMembersForUser(arg0 string) ([]*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMembersForUser", arg0) + ret0, _ := ret[0].([]*model.BoardMember) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMembersForUser indicates an expected call of GetMembersForUser. +func (mr *MockStoreMockRecorder) GetMembersForUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembersForUser", reflect.TypeOf((*MockStore)(nil).GetMembersForUser), arg0) } // GetNextNotificationHint mocks base method. @@ -376,33 +596,18 @@ func (mr *MockStoreMockRecorder) GetNextNotificationHint(arg0 interface{}) *gomo } // GetNotificationHint mocks base method. -func (m *MockStore) GetNotificationHint(arg0 store.Container, arg1 string) (*model.NotificationHint, error) { +func (m *MockStore) GetNotificationHint(arg0 string) (*model.NotificationHint, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationHint", arg0, arg1) + ret := m.ctrl.Call(m, "GetNotificationHint", arg0) ret0, _ := ret[0].(*model.NotificationHint) ret1, _ := ret[1].(error) return ret0, ret1 } // GetNotificationHint indicates an expected call of GetNotificationHint. -func (mr *MockStoreMockRecorder) GetNotificationHint(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNotificationHint(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationHint", reflect.TypeOf((*MockStore)(nil).GetNotificationHint), arg0, arg1) -} - -// GetParentID mocks base method. -func (m *MockStore) GetParentID(arg0 store.Container, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetParentID", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetParentID indicates an expected call of GetParentID. -func (mr *MockStoreMockRecorder) GetParentID(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParentID", reflect.TypeOf((*MockStore)(nil).GetParentID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationHint", reflect.TypeOf((*MockStore)(nil).GetNotificationHint), arg0) } // GetRegisteredUserCount mocks base method. @@ -420,21 +625,6 @@ func (mr *MockStoreMockRecorder) GetRegisteredUserCount() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegisteredUserCount", reflect.TypeOf((*MockStore)(nil).GetRegisteredUserCount)) } -// GetRootID mocks base method. -func (m *MockStore) GetRootID(arg0 store.Container, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRootID", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRootID indicates an expected call of GetRootID. -func (mr *MockStoreMockRecorder) GetRootID(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRootID", reflect.TypeOf((*MockStore)(nil).GetRootID), arg0, arg1) -} - // GetSession mocks base method. func (m *MockStore) GetSession(arg0 string, arg1 int64) (*model.Session, error) { m.ctrl.T.Helper() @@ -451,22 +641,22 @@ func (mr *MockStoreMockRecorder) GetSession(arg0, arg1 interface{}) *gomock.Call } // GetSharing mocks base method. -func (m *MockStore) GetSharing(arg0 store.Container, arg1 string) (*model.Sharing, error) { +func (m *MockStore) GetSharing(arg0 string) (*model.Sharing, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSharing", arg0, arg1) + ret := m.ctrl.Call(m, "GetSharing", arg0) ret0, _ := ret[0].(*model.Sharing) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSharing indicates an expected call of GetSharing. -func (mr *MockStoreMockRecorder) GetSharing(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSharing(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSharing", reflect.TypeOf((*MockStore)(nil).GetSharing), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSharing", reflect.TypeOf((*MockStore)(nil).GetSharing), arg0) } // GetSubTree2 mocks base method. -func (m *MockStore) GetSubTree2(arg0 store.Container, arg1 string, arg2 model.QuerySubtreeOptions) ([]model.Block, error) { +func (m *MockStore) GetSubTree2(arg0, arg1 string, arg2 model.QuerySubtreeOptions) ([]model.Block, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSubTree2", arg0, arg1, arg2) ret0, _ := ret[0].([]model.Block) @@ -481,7 +671,7 @@ func (mr *MockStoreMockRecorder) GetSubTree2(arg0, arg1, arg2 interface{}) *gomo } // GetSubTree3 mocks base method. -func (m *MockStore) GetSubTree3(arg0 store.Container, arg1 string, arg2 model.QuerySubtreeOptions) ([]model.Block, error) { +func (m *MockStore) GetSubTree3(arg0, arg1 string, arg2 model.QuerySubtreeOptions) ([]model.Block, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSubTree3", arg0, arg1, arg2) ret0, _ := ret[0].([]model.Block) @@ -496,63 +686,63 @@ func (mr *MockStoreMockRecorder) GetSubTree3(arg0, arg1, arg2 interface{}) *gomo } // GetSubscribersCountForBlock mocks base method. -func (m *MockStore) GetSubscribersCountForBlock(arg0 store.Container, arg1 string) (int, error) { +func (m *MockStore) GetSubscribersCountForBlock(arg0 string) (int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscribersCountForBlock", arg0, arg1) + ret := m.ctrl.Call(m, "GetSubscribersCountForBlock", arg0) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSubscribersCountForBlock indicates an expected call of GetSubscribersCountForBlock. -func (mr *MockStoreMockRecorder) GetSubscribersCountForBlock(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSubscribersCountForBlock(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscribersCountForBlock", reflect.TypeOf((*MockStore)(nil).GetSubscribersCountForBlock), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscribersCountForBlock", reflect.TypeOf((*MockStore)(nil).GetSubscribersCountForBlock), arg0) } // GetSubscribersForBlock mocks base method. -func (m *MockStore) GetSubscribersForBlock(arg0 store.Container, arg1 string) ([]*model.Subscriber, error) { +func (m *MockStore) GetSubscribersForBlock(arg0 string) ([]*model.Subscriber, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscribersForBlock", arg0, arg1) + ret := m.ctrl.Call(m, "GetSubscribersForBlock", arg0) ret0, _ := ret[0].([]*model.Subscriber) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSubscribersForBlock indicates an expected call of GetSubscribersForBlock. -func (mr *MockStoreMockRecorder) GetSubscribersForBlock(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSubscribersForBlock(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscribersForBlock", reflect.TypeOf((*MockStore)(nil).GetSubscribersForBlock), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscribersForBlock", reflect.TypeOf((*MockStore)(nil).GetSubscribersForBlock), arg0) } // GetSubscription mocks base method. -func (m *MockStore) GetSubscription(arg0 store.Container, arg1, arg2 string) (*model.Subscription, error) { +func (m *MockStore) GetSubscription(arg0, arg1 string) (*model.Subscription, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscription", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetSubscription", arg0, arg1) ret0, _ := ret[0].(*model.Subscription) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSubscription indicates an expected call of GetSubscription. -func (mr *MockStoreMockRecorder) GetSubscription(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSubscription(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscription", reflect.TypeOf((*MockStore)(nil).GetSubscription), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscription", reflect.TypeOf((*MockStore)(nil).GetSubscription), arg0, arg1) } // GetSubscriptions mocks base method. -func (m *MockStore) GetSubscriptions(arg0 store.Container, arg1 string) ([]*model.Subscription, error) { +func (m *MockStore) GetSubscriptions(arg0 string) ([]*model.Subscription, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscriptions", arg0, arg1) + ret := m.ctrl.Call(m, "GetSubscriptions", arg0) ret0, _ := ret[0].([]*model.Subscription) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSubscriptions indicates an expected call of GetSubscriptions. -func (mr *MockStoreMockRecorder) GetSubscriptions(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSubscriptions(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptions", reflect.TypeOf((*MockStore)(nil).GetSubscriptions), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptions", reflect.TypeOf((*MockStore)(nil).GetSubscriptions), arg0) } // GetSystemSetting mocks base method. @@ -585,6 +775,66 @@ func (mr *MockStoreMockRecorder) GetSystemSettings() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSystemSettings", reflect.TypeOf((*MockStore)(nil).GetSystemSettings)) } +// GetTeam mocks base method. +func (m *MockStore) GetTeam(arg0 string) (*model.Team, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeam", arg0) + ret0, _ := ret[0].(*model.Team) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTeam indicates an expected call of GetTeam. +func (mr *MockStoreMockRecorder) GetTeam(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeam", reflect.TypeOf((*MockStore)(nil).GetTeam), arg0) +} + +// GetTeamCount mocks base method. +func (m *MockStore) GetTeamCount() (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamCount") + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTeamCount indicates an expected call of GetTeamCount. +func (mr *MockStoreMockRecorder) GetTeamCount() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamCount", reflect.TypeOf((*MockStore)(nil).GetTeamCount)) +} + +// GetTeamsForUser mocks base method. +func (m *MockStore) GetTeamsForUser(arg0 string) ([]*model.Team, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamsForUser", arg0) + ret0, _ := ret[0].([]*model.Team) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTeamsForUser indicates an expected call of GetTeamsForUser. +func (mr *MockStoreMockRecorder) GetTeamsForUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsForUser", reflect.TypeOf((*MockStore)(nil).GetTeamsForUser), arg0) +} + +// GetTemplateBoards mocks base method. +func (m *MockStore) GetTemplateBoards(arg0 string) ([]*model.Board, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTemplateBoards", arg0) + ret0, _ := ret[0].([]*model.Board) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTemplateBoards indicates an expected call of GetTemplateBoards. +func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0) +} + // GetUserByEmail mocks base method. func (m *MockStore) GetUserByEmail(arg0 string) (*model.User, error) { m.ctrl.T.Helper() @@ -630,107 +880,93 @@ func (mr *MockStoreMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockStore)(nil).GetUserByUsername), arg0) } -// GetUserWorkspaces mocks base method. -func (m *MockStore) GetUserWorkspaces(arg0 string) ([]model.UserWorkspace, error) { +// GetUserCategoryBlocks mocks base method. +func (m *MockStore) GetUserCategoryBlocks(arg0, arg1 string) ([]model.CategoryBlocks, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserWorkspaces", arg0) - ret0, _ := ret[0].([]model.UserWorkspace) + ret := m.ctrl.Call(m, "GetUserCategoryBlocks", arg0, arg1) + ret0, _ := ret[0].([]model.CategoryBlocks) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetUserWorkspaces indicates an expected call of GetUserWorkspaces. -func (mr *MockStoreMockRecorder) GetUserWorkspaces(arg0 interface{}) *gomock.Call { +// GetUserCategoryBlocks indicates an expected call of GetUserCategoryBlocks. +func (mr *MockStoreMockRecorder) GetUserCategoryBlocks(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserWorkspaces", reflect.TypeOf((*MockStore)(nil).GetUserWorkspaces), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCategoryBlocks", reflect.TypeOf((*MockStore)(nil).GetUserCategoryBlocks), arg0, arg1) } -// GetUsersByWorkspace mocks base method. -func (m *MockStore) GetUsersByWorkspace(arg0 string) ([]*model.User, error) { +// GetUsersByTeam mocks base method. +func (m *MockStore) GetUsersByTeam(arg0 string) ([]*model.User, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersByWorkspace", arg0) + ret := m.ctrl.Call(m, "GetUsersByTeam", arg0) ret0, _ := ret[0].([]*model.User) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetUsersByWorkspace indicates an expected call of GetUsersByWorkspace. -func (mr *MockStoreMockRecorder) GetUsersByWorkspace(arg0 interface{}) *gomock.Call { +// GetUsersByTeam indicates an expected call of GetUsersByTeam. +func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByWorkspace", reflect.TypeOf((*MockStore)(nil).GetUsersByWorkspace), arg0) -} - -// GetWorkspace mocks base method. -func (m *MockStore) GetWorkspace(arg0 string) (*model.Workspace, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspace", arg0) - ret0, _ := ret[0].(*model.Workspace) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetWorkspace indicates an expected call of GetWorkspace. -func (mr *MockStoreMockRecorder) GetWorkspace(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspace", reflect.TypeOf((*MockStore)(nil).GetWorkspace), arg0) -} - -// GetWorkspaceCount mocks base method. -func (m *MockStore) GetWorkspaceCount() (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceCount") - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetWorkspaceCount indicates an expected call of GetWorkspaceCount. -func (mr *MockStoreMockRecorder) GetWorkspaceCount() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceCount", reflect.TypeOf((*MockStore)(nil).GetWorkspaceCount)) -} - -// HasWorkspaceAccess mocks base method. -func (m *MockStore) HasWorkspaceAccess(arg0, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasWorkspaceAccess", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// HasWorkspaceAccess indicates an expected call of HasWorkspaceAccess. -func (mr *MockStoreMockRecorder) HasWorkspaceAccess(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasWorkspaceAccess", reflect.TypeOf((*MockStore)(nil).HasWorkspaceAccess), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0) } // InsertBlock mocks base method. -func (m *MockStore) InsertBlock(arg0 store.Container, arg1 *model.Block, arg2 string) error { +func (m *MockStore) InsertBlock(arg0 *model.Block, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertBlock", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "InsertBlock", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // InsertBlock indicates an expected call of InsertBlock. -func (mr *MockStoreMockRecorder) InsertBlock(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertBlock(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlock", reflect.TypeOf((*MockStore)(nil).InsertBlock), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlock", reflect.TypeOf((*MockStore)(nil).InsertBlock), arg0, arg1) } // InsertBlocks mocks base method. -func (m *MockStore) InsertBlocks(arg0 store.Container, arg1 []model.Block, arg2 string) error { +func (m *MockStore) InsertBlocks(arg0 []model.Block, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertBlocks", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "InsertBlocks", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // InsertBlocks indicates an expected call of InsertBlocks. -func (mr *MockStoreMockRecorder) InsertBlocks(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertBlocks(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlocks", reflect.TypeOf((*MockStore)(nil).InsertBlocks), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlocks", reflect.TypeOf((*MockStore)(nil).InsertBlocks), arg0, arg1) +} + +// InsertBoard mocks base method. +func (m *MockStore) InsertBoard(arg0 *model.Board, arg1 string) (*model.Board, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertBoard", arg0, arg1) + ret0, _ := ret[0].(*model.Board) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertBoard indicates an expected call of InsertBoard. +func (mr *MockStoreMockRecorder) InsertBoard(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBoard", reflect.TypeOf((*MockStore)(nil).InsertBoard), arg0, arg1) +} + +// InsertBoardWithAdmin mocks base method. +func (m *MockStore) InsertBoardWithAdmin(arg0 *model.Board, arg1 string) (*model.Board, *model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertBoardWithAdmin", arg0, arg1) + ret0, _ := ret[0].(*model.Board) + ret1, _ := ret[1].(*model.BoardMember) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// InsertBoardWithAdmin indicates an expected call of InsertBoardWithAdmin. +func (mr *MockStoreMockRecorder) InsertBoardWithAdmin(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBoardWithAdmin", reflect.TypeOf((*MockStore)(nil).InsertBoardWithAdmin), arg0, arg1) } // IsErrNotFound mocks base method. @@ -748,31 +984,61 @@ func (mr *MockStoreMockRecorder) IsErrNotFound(arg0 interface{}) *gomock.Call { } // PatchBlock mocks base method. -func (m *MockStore) PatchBlock(arg0 store.Container, arg1 string, arg2 *model.BlockPatch, arg3 string) error { +func (m *MockStore) PatchBlock(arg0 string, arg1 *model.BlockPatch, arg2 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBlock", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "PatchBlock", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // PatchBlock indicates an expected call of PatchBlock. -func (mr *MockStoreMockRecorder) PatchBlock(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) PatchBlock(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBlock", reflect.TypeOf((*MockStore)(nil).PatchBlock), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBlock", reflect.TypeOf((*MockStore)(nil).PatchBlock), arg0, arg1, arg2) } // PatchBlocks mocks base method. -func (m *MockStore) PatchBlocks(arg0 store.Container, arg1 *model.BlockPatchBatch, arg2 string) error { +func (m *MockStore) PatchBlocks(arg0 *model.BlockPatchBatch, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBlocks", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "PatchBlocks", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // PatchBlocks indicates an expected call of PatchBlocks. -func (mr *MockStoreMockRecorder) PatchBlocks(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) PatchBlocks(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBlocks", reflect.TypeOf((*MockStore)(nil).PatchBlocks), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBlocks", reflect.TypeOf((*MockStore)(nil).PatchBlocks), arg0, arg1) +} + +// PatchBoard mocks base method. +func (m *MockStore) PatchBoard(arg0 string, arg1 *model.BoardPatch, arg2 string) (*model.Board, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PatchBoard", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.Board) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PatchBoard indicates an expected call of PatchBoard. +func (mr *MockStoreMockRecorder) PatchBoard(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBoard", reflect.TypeOf((*MockStore)(nil).PatchBoard), arg0, arg1, arg2) +} + +// PatchBoardsAndBlocks mocks base method. +func (m *MockStore) PatchBoardsAndBlocks(arg0 *model.PatchBoardsAndBlocks, arg1 string) (*model.BoardsAndBlocks, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PatchBoardsAndBlocks", arg0, arg1) + ret0, _ := ret[0].(*model.BoardsAndBlocks) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PatchBoardsAndBlocks indicates an expected call of PatchBoardsAndBlocks. +func (mr *MockStoreMockRecorder) PatchBoardsAndBlocks(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBoardsAndBlocks", reflect.TypeOf((*MockStore)(nil).PatchBoardsAndBlocks), arg0, arg1) } // PatchUserProps mocks base method. @@ -804,7 +1070,7 @@ func (mr *MockStoreMockRecorder) RefreshSession(arg0 interface{}) *gomock.Call { } // RemoveDefaultTemplates mocks base method. -func (m *MockStore) RemoveDefaultTemplates(arg0 []model.Block) error { +func (m *MockStore) RemoveDefaultTemplates(arg0 []*model.Board) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RemoveDefaultTemplates", arg0) ret0, _ := ret[0].(error) @@ -817,6 +1083,51 @@ func (mr *MockStoreMockRecorder) RemoveDefaultTemplates(arg0 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveDefaultTemplates", reflect.TypeOf((*MockStore)(nil).RemoveDefaultTemplates), arg0) } +// SaveMember mocks base method. +func (m *MockStore) SaveMember(arg0 *model.BoardMember) (*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveMember", arg0) + ret0, _ := ret[0].(*model.BoardMember) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveMember indicates an expected call of SaveMember. +func (mr *MockStoreMockRecorder) SaveMember(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveMember", reflect.TypeOf((*MockStore)(nil).SaveMember), arg0) +} + +// SearchBoardsForUserAndTeam mocks base method. +func (m *MockStore) SearchBoardsForUserAndTeam(arg0, arg1, arg2 string) ([]*model.Board, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchBoardsForUserAndTeam", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.Board) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchBoardsForUserAndTeam indicates an expected call of SearchBoardsForUserAndTeam. +func (mr *MockStoreMockRecorder) SearchBoardsForUserAndTeam(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUserAndTeam), arg0, arg1, arg2) +} + +// SearchUsersByTeam mocks base method. +func (m *MockStore) SearchUsersByTeam(arg0, arg1 string) ([]*model.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1) + ret0, _ := ret[0].([]*model.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchUsersByTeam indicates an expected call of SearchUsersByTeam. +func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1) +} + // SetSystemSetting mocks base method. func (m *MockStore) SetSystemSetting(arg0, arg1 string) error { m.ctrl.T.Helper() @@ -846,17 +1157,31 @@ func (mr *MockStoreMockRecorder) Shutdown() *gomock.Call { } // UndeleteBlock mocks base method. -func (m *MockStore) UndeleteBlock(arg0 store.Container, arg1, arg2 string) error { +func (m *MockStore) UndeleteBlock(arg0, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UndeleteBlock", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "UndeleteBlock", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // UndeleteBlock indicates an expected call of UndeleteBlock. -func (mr *MockStoreMockRecorder) UndeleteBlock(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UndeleteBlock(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndeleteBlock", reflect.TypeOf((*MockStore)(nil).UndeleteBlock), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndeleteBlock", reflect.TypeOf((*MockStore)(nil).UndeleteBlock), arg0, arg1) +} + +// UpdateCategory mocks base method. +func (m *MockStore) UpdateCategory(arg0 model.Category) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateCategory", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateCategory indicates an expected call of UpdateCategory. +func (mr *MockStoreMockRecorder) UpdateCategory(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCategory", reflect.TypeOf((*MockStore)(nil).UpdateCategory), arg0) } // UpdateSession mocks base method. @@ -874,17 +1199,17 @@ func (mr *MockStoreMockRecorder) UpdateSession(arg0 interface{}) *gomock.Call { } // UpdateSubscribersNotifiedAt mocks base method. -func (m *MockStore) UpdateSubscribersNotifiedAt(arg0 store.Container, arg1 string, arg2 int64) error { +func (m *MockStore) UpdateSubscribersNotifiedAt(arg0 string, arg1 int64) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateSubscribersNotifiedAt", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "UpdateSubscribersNotifiedAt", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // UpdateSubscribersNotifiedAt indicates an expected call of UpdateSubscribersNotifiedAt. -func (mr *MockStoreMockRecorder) UpdateSubscribersNotifiedAt(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateSubscribersNotifiedAt(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubscribersNotifiedAt", reflect.TypeOf((*MockStore)(nil).UpdateSubscribersNotifiedAt), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubscribersNotifiedAt", reflect.TypeOf((*MockStore)(nil).UpdateSubscribersNotifiedAt), arg0, arg1) } // UpdateUser mocks base method. @@ -945,43 +1270,43 @@ func (mr *MockStoreMockRecorder) UpsertNotificationHint(arg0, arg1 interface{}) } // UpsertSharing mocks base method. -func (m *MockStore) UpsertSharing(arg0 store.Container, arg1 model.Sharing) error { +func (m *MockStore) UpsertSharing(arg0 model.Sharing) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertSharing", arg0, arg1) + ret := m.ctrl.Call(m, "UpsertSharing", arg0) ret0, _ := ret[0].(error) return ret0 } // UpsertSharing indicates an expected call of UpsertSharing. -func (mr *MockStoreMockRecorder) UpsertSharing(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertSharing(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertSharing", reflect.TypeOf((*MockStore)(nil).UpsertSharing), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertSharing", reflect.TypeOf((*MockStore)(nil).UpsertSharing), arg0) } -// UpsertWorkspaceSettings mocks base method. -func (m *MockStore) UpsertWorkspaceSettings(arg0 model.Workspace) error { +// UpsertTeamSettings mocks base method. +func (m *MockStore) UpsertTeamSettings(arg0 model.Team) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertWorkspaceSettings", arg0) + ret := m.ctrl.Call(m, "UpsertTeamSettings", arg0) ret0, _ := ret[0].(error) return ret0 } -// UpsertWorkspaceSettings indicates an expected call of UpsertWorkspaceSettings. -func (mr *MockStoreMockRecorder) UpsertWorkspaceSettings(arg0 interface{}) *gomock.Call { +// UpsertTeamSettings indicates an expected call of UpsertTeamSettings. +func (mr *MockStoreMockRecorder) UpsertTeamSettings(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSettings", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSettings), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTeamSettings", reflect.TypeOf((*MockStore)(nil).UpsertTeamSettings), arg0) } -// UpsertWorkspaceSignupToken mocks base method. -func (m *MockStore) UpsertWorkspaceSignupToken(arg0 model.Workspace) error { +// UpsertTeamSignupToken mocks base method. +func (m *MockStore) UpsertTeamSignupToken(arg0 model.Team) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertWorkspaceSignupToken", arg0) + ret := m.ctrl.Call(m, "UpsertTeamSignupToken", arg0) ret0, _ := ret[0].(error) return ret0 } -// UpsertWorkspaceSignupToken indicates an expected call of UpsertWorkspaceSignupToken. -func (mr *MockStoreMockRecorder) UpsertWorkspaceSignupToken(arg0 interface{}) *gomock.Call { +// UpsertTeamSignupToken indicates an expected call of UpsertTeamSignupToken. +func (mr *MockStoreMockRecorder) UpsertTeamSignupToken(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSignupToken", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSignupToken), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTeamSignupToken", reflect.TypeOf((*MockStore)(nil).UpsertTeamSignupToken), arg0) } diff --git a/server/services/store/sqlstore/blocks.go b/server/services/store/sqlstore/blocks.go index d0874faec..237e7e54d 100644 --- a/server/services/store/sqlstore/blocks.go +++ b/server/services/store/sqlstore/blocks.go @@ -5,12 +5,12 @@ import ( "encoding/json" "fmt" + "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" sq "github.com/Masterminds/squirrel" _ "github.com/lib/pq" // postgres driver "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" _ "github.com/mattn/go-sqlite3" // sqlite driver "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -20,10 +20,10 @@ const ( maxSearchDepth = 50 ) -type RootIDNilError struct{} +type BoardIDNilError struct{} -func (re RootIDNilError) Error() string { - return "rootId is nil" +func (re BoardIDNilError) Error() string { + return "boardID is nil" } type BlockNotFoundErr struct { @@ -36,9 +36,9 @@ func (be BlockNotFoundErr) Error() string { func (s *SQLStore) timestampToCharField(name string, as string) string { switch s.dbType { - case mysqlDBType: + case model.MysqlDBType: return fmt.Sprintf("date_format(%s, '%%Y-%%m-%%d %%H:%%i:%%S') AS %s", name, as) - case postgresDBType: + case model.PostgresDBType: return fmt.Sprintf("to_char(%s, 'YYYY-MM-DD HH:MI:SS.MS') AS %s", name, as) default: return fmt.Sprintf("%s AS %s", name, as) @@ -49,7 +49,6 @@ func (s *SQLStore) blockFields() []string { return []string{ "id", "parent_id", - "root_id", "created_by", "modified_by", s.escapeField("schema"), @@ -60,15 +59,15 @@ func (s *SQLStore) blockFields() []string { "create_at", "update_at", "delete_at", - "COALESCE(workspace_id, '0')", + "COALESCE(board_id, '0')", } } -func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, c store.Container, parentID string, blockType string) ([]model.Block, error) { +func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentID string, blockType string) ([]model.Block, error) { query := s.getQueryBuilder(db). Select(s.blockFields()...). From(s.tablePrefix + "blocks"). - Where(sq.Eq{"COALESCE(workspace_id, '0')": c.WorkspaceID}). + Where(sq.Eq{"board_id": boardID}). Where(sq.Eq{"parent_id": parentID}). Where(sq.Eq{"type": blockType}) @@ -83,12 +82,12 @@ func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, c store.Containe return s.blocksFromRows(rows) } -func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, c store.Container, parentID string) ([]model.Block, error) { +func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID string) ([]model.Block, error) { query := s.getQueryBuilder(db). Select(s.blockFields()...). From(s.tablePrefix + "blocks"). Where(sq.Eq{"parent_id": parentID}). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}) + Where(sq.Eq{"board_id": boardID}) rows, err := query.Query() if err != nil { @@ -101,16 +100,15 @@ func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, c store.Container, pare return s.blocksFromRows(rows) } -func (s *SQLStore) getBlocksWithRootID(db sq.BaseRunner, c store.Container, rootID string) ([]model.Block, error) { +func (s *SQLStore) getBlocksWithBoardID(db sq.BaseRunner, boardID string) ([]model.Block, error) { query := s.getQueryBuilder(db). Select(s.blockFields()...). From(s.tablePrefix + "blocks"). - Where(sq.Eq{"root_id": rootID}). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}) + Where(sq.Eq{"board_id": boardID}) rows, err := query.Query() if err != nil { - s.logger.Error(`GetBlocksWithRootID ERROR`, mlog.Err(err)) + s.logger.Error(`GetBlocksWithBoardID ERROR`, mlog.Err(err)) return nil, err } @@ -119,12 +117,12 @@ func (s *SQLStore) getBlocksWithRootID(db sq.BaseRunner, c store.Container, root return s.blocksFromRows(rows) } -func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, c store.Container, blockType string) ([]model.Block, error) { +func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, boardID, blockType string) ([]model.Block, error) { query := s.getQueryBuilder(db). Select(s.blockFields()...). From(s.tablePrefix + "blocks"). Where(sq.Eq{"type": blockType}). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}) + Where(sq.Eq{"board_id": boardID}) rows, err := query.Query() if err != nil { @@ -138,13 +136,13 @@ func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, c store.Container, blockT } // getSubTree2 returns blocks within 2 levels of the given blockID. -func (s *SQLStore) getSubTree2(db sq.BaseRunner, c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { +func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { query := s.getQueryBuilder(db). Select(s.blockFields()...). From(s.tablePrefix + "blocks"). Where(sq.Or{sq.Eq{"id": blockID}, sq.Eq{"parent_id": blockID}}). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}). - OrderBy("insert_at") + Where(sq.Eq{"board_id": boardID}). + OrderBy("insert_at, update_at") if opts.BeforeUpdateAt != 0 { query = query.Where(sq.LtOrEq{"update_at": opts.BeforeUpdateAt}) @@ -170,12 +168,11 @@ func (s *SQLStore) getSubTree2(db sq.BaseRunner, c store.Container, blockID stri } // getSubTree3 returns blocks within 3 levels of the given blockID. -func (s *SQLStore) getSubTree3(db sq.BaseRunner, c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { +func (s *SQLStore) getSubTree3(db sq.BaseRunner, boardID string, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { // This first subquery returns repeated blocks query := s.getQueryBuilder(db).Select( "l3.id", "l3.parent_id", - "l3.root_id", "l3.created_by", "l3.modified_by", "l3."+s.escapeField("schema"), @@ -186,14 +183,14 @@ func (s *SQLStore) getSubTree3(db sq.BaseRunner, c store.Container, blockID stri "l3.create_at", "l3.update_at", "l3.delete_at", - "COALESCE(l3.workspace_id, '0')", + "l3.board_id", ). From(s.tablePrefix + "blocks" + " as l1"). Join(s.tablePrefix + "blocks" + " as l2 on l2.parent_id = l1.id or l2.id = l1.id"). Join(s.tablePrefix + "blocks" + " as l3 on l3.parent_id = l2.id or l3.id = l2.id"). Where(sq.Eq{"l1.id": blockID}). - Where(sq.Eq{"COALESCE(l3.workspace_id, '0')": c.WorkspaceID}). - OrderBy("l3.id, insertAt") + Where(sq.Eq{"l3.board_id": boardID}). + OrderBy("insertAt,l3.id") if opts.BeforeUpdateAt != 0 { query = query.Where(sq.LtOrEq{"update_at": opts.BeforeUpdateAt}) @@ -203,8 +200,8 @@ func (s *SQLStore) getSubTree3(db sq.BaseRunner, c store.Container, blockID stri query = query.Where(sq.GtOrEq{"update_at": opts.AfterUpdateAt}) } - if s.dbType == postgresDBType { - query = query.Options("DISTINCT ON (l3.id)") + if s.dbType == model.PostgresDBType { + query = query.Options("DISTINCT ON (insertAt,l3.id)") } else { query = query.Distinct() } @@ -224,16 +221,15 @@ func (s *SQLStore) getSubTree3(db sq.BaseRunner, c store.Container, blockID stri return s.blocksFromRows(rows) } -func (s *SQLStore) getAllBlocks(db sq.BaseRunner, c store.Container) ([]model.Block, error) { +func (s *SQLStore) getBlocksForBoard(db sq.BaseRunner, boardID string) ([]model.Block, error) { query := s.getQueryBuilder(db). Select(s.blockFields()...). From(s.tablePrefix + "blocks"). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}) + Where(sq.Eq{"board_id": boardID}) rows, err := query.Query() if err != nil { - s.logger.Error(`getAllBlocks ERROR`, mlog.Err(err)) - + s.logger.Error(`getAllBlocksForBoard ERROR`, mlog.Err(err)) return nil, err } defer s.CloseRows(rows) @@ -253,7 +249,6 @@ func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]model.Block, error) { err := rows.Scan( &block.ID, &block.ParentID, - &block.RootID, &block.CreatedBy, &modifiedBy, &block.Schema, @@ -264,7 +259,7 @@ func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]model.Block, error) { &block.CreateAt, &block.UpdateAt, &block.DeleteAt, - &block.WorkspaceID) + &block.BoardID) if err != nil { // handle this error s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err)) @@ -290,45 +285,9 @@ func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]model.Block, error) { return results, nil } -func (s *SQLStore) getRootID(db sq.BaseRunner, c store.Container, blockID string) (string, error) { - query := s.getQueryBuilder(db).Select("root_id"). - From(s.tablePrefix + "blocks"). - Where(sq.Eq{"id": blockID}). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}) - - row := query.QueryRow() - - var rootID string - - err := row.Scan(&rootID) - if err != nil { - return "", err - } - - return rootID, nil -} - -func (s *SQLStore) getParentID(db sq.BaseRunner, c store.Container, blockID string) (string, error) { - query := s.getQueryBuilder(db).Select("parent_id"). - From(s.tablePrefix + "blocks"). - Where(sq.Eq{"id": blockID}). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}) - - row := query.QueryRow() - - var parentID string - - err := row.Scan(&parentID) - if err != nil { - return "", err - } - - return parentID, nil -} - -func (s *SQLStore) insertBlock(db sq.BaseRunner, c store.Container, block *model.Block, userID string) error { - if block.RootID == "" { - return RootIDNilError{} +func (s *SQLStore) insertBlock(db sq.BaseRunner, block *model.Block, userID string) error { + if block.BoardID == "" { + return BoardIDNilError{} } fieldsJSON, err := json.Marshal(block.Fields) @@ -336,7 +295,7 @@ func (s *SQLStore) insertBlock(db sq.BaseRunner, c store.Container, block *model return err } - existingBlock, err := s.getBlock(db, c, block.ID) + existingBlock, err := s.getBlock(db, block.ID) if err != nil { return err } @@ -346,10 +305,9 @@ func (s *SQLStore) insertBlock(db sq.BaseRunner, c store.Container, block *model insertQuery := s.getQueryBuilder(db).Insert(""). Columns( - "workspace_id", + "channel_id", "id", "parent_id", - "root_id", "created_by", "modified_by", s.escapeField("schema"), @@ -359,31 +317,31 @@ func (s *SQLStore) insertBlock(db sq.BaseRunner, c store.Container, block *model "create_at", "update_at", "delete_at", + "board_id", ) insertQueryValues := map[string]interface{}{ - "workspace_id": c.WorkspaceID, + "channel_id": "", "id": block.ID, "parent_id": block.ParentID, - "root_id": block.RootID, s.escapeField("schema"): block.Schema, "type": block.Type, "title": block.Title, "fields": fieldsJSON, "delete_at": block.DeleteAt, - "created_by": block.CreatedBy, + "created_by": userID, "modified_by": block.ModifiedBy, - "create_at": block.CreateAt, + "create_at": utils.GetMillis(), "update_at": block.UpdateAt, + "board_id": block.BoardID, } if existingBlock != nil { // block with ID exists, so this is an update operation query := s.getQueryBuilder(db).Update(s.tablePrefix+"blocks"). Where(sq.Eq{"id": block.ID}). - Where(sq.Eq{"COALESCE(workspace_id, '0')": c.WorkspaceID}). + Where(sq.Eq{"board_id": block.BoardID}). Set("parent_id", block.ParentID). - Set("root_id", block.RootID). Set("modified_by", block.ModifiedBy). Set(s.escapeField("schema"), block.Schema). Set("type", block.Type). @@ -394,17 +352,11 @@ func (s *SQLStore) insertBlock(db sq.BaseRunner, c store.Container, block *model if _, err := query.Exec(); err != nil { s.logger.Error(`InsertBlock error occurred while updating existing block`, mlog.String("blockID", block.ID), mlog.Err(err)) + return err } } else { block.CreatedBy = userID - block.CreateAt = utils.GetMillis() - - insertQueryValues["created_by"] = block.CreatedBy - insertQueryValues["create_at"] = block.CreateAt - insertQueryValues["update_at"] = block.UpdateAt - insertQueryValues["modified_by"] = block.ModifiedBy - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks") if _, err := query.Exec(); err != nil { return err @@ -420,8 +372,8 @@ func (s *SQLStore) insertBlock(db sq.BaseRunner, c store.Container, block *model return nil } -func (s *SQLStore) patchBlock(db sq.BaseRunner, c store.Container, blockID string, blockPatch *model.BlockPatch, userID string) error { - existingBlock, err := s.getBlock(db, c, blockID) +func (s *SQLStore) patchBlock(db sq.BaseRunner, blockID string, blockPatch *model.BlockPatch, userID string) error { + existingBlock, err := s.getBlock(db, blockID) if err != nil { return err } @@ -430,12 +382,12 @@ func (s *SQLStore) patchBlock(db sq.BaseRunner, c store.Container, blockID strin } block := blockPatch.Patch(existingBlock) - return s.insertBlock(db, c, block, userID) + return s.insertBlock(db, block, userID) } -func (s *SQLStore) patchBlocks(db sq.BaseRunner, c store.Container, blockPatches *model.BlockPatchBatch, userID string) error { +func (s *SQLStore) patchBlocks(db sq.BaseRunner, blockPatches *model.BlockPatchBatch, userID string) error { for i, blockID := range blockPatches.BlockIDs { - err := s.patchBlock(db, c, blockID, &blockPatches.BlockPatches[i], userID) + err := s.patchBlock(db, blockID, &blockPatches.BlockPatches[i], userID) if err != nil { return err } @@ -443,14 +395,14 @@ func (s *SQLStore) patchBlocks(db sq.BaseRunner, c store.Container, blockPatches return nil } -func (s *SQLStore) insertBlocks(db sq.BaseRunner, c store.Container, blocks []model.Block, userID string) error { +func (s *SQLStore) insertBlocks(db sq.BaseRunner, blocks []model.Block, userID string) error { for _, block := range blocks { - if block.RootID == "" { - return RootIDNilError{} + if block.BoardID == "" { + return BoardIDNilError{} } } for i := range blocks { - err := s.insertBlock(db, c, &blocks[i], userID) + err := s.insertBlock(db, &blocks[i], userID) if err != nil { return err } @@ -458,13 +410,14 @@ func (s *SQLStore) insertBlocks(db sq.BaseRunner, c store.Container, blocks []mo return nil } -func (s *SQLStore) deleteBlock(db sq.BaseRunner, c store.Container, blockID string, modifiedBy string) error { - block, err := s.getBlock(db, c, blockID) +func (s *SQLStore) deleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error { + block, err := s.getBlock(db, blockID) if err != nil { return err } if block == nil { + s.logger.Warn("deleteBlock block not found", mlog.String("block_id", blockID)) return nil // deleting non-exiting block is not considered an error (for now) } @@ -476,14 +429,13 @@ func (s *SQLStore) deleteBlock(db sq.BaseRunner, c store.Container, blockID stri now := utils.GetMillis() insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix+"blocks_history"). Columns( - "workspace_id", + "board_id", "id", "parent_id", s.escapeField("schema"), "type", "title", "fields", - "root_id", "modified_by", "create_at", "update_at", @@ -491,14 +443,13 @@ func (s *SQLStore) deleteBlock(db sq.BaseRunner, c store.Container, blockID stri "created_by", ). Values( - c.WorkspaceID, + block.BoardID, block.ID, block.ParentID, block.Schema, block.Type, block.Title, fieldsJSON, - block.RootID, modifiedBy, block.CreateAt, now, @@ -512,8 +463,7 @@ func (s *SQLStore) deleteBlock(db sq.BaseRunner, c store.Container, blockID stri deleteQuery := s.getQueryBuilder(db). Delete(s.tablePrefix + "blocks"). - Where(sq.Eq{"id": blockID}). - Where(sq.Eq{"COALESCE(workspace_id, '0')": c.WorkspaceID}) + Where(sq.Eq{"id": blockID}) if _, err := deleteQuery.Exec(); err != nil { return err @@ -522,18 +472,20 @@ func (s *SQLStore) deleteBlock(db sq.BaseRunner, c store.Container, blockID stri return nil } -func (s *SQLStore) undeleteBlock(db sq.BaseRunner, c store.Container, blockID string, modifiedBy string) error { - blocks, err := s.getBlockHistory(db, c, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true}) +func (s *SQLStore) undeleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error { + blocks, err := s.getBlockHistory(db, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true}) if err != nil { return err } if len(blocks) == 0 { + s.logger.Warn("undeleteBlock block not found", mlog.String("block_id", blockID)) return nil // deleting non-exiting block is not considered an error (for now) } block := blocks[0] if block.DeleteAt == 0 { + s.logger.Warn("undeleteBlock block not deleted", mlog.String("block_id", block.ID)) return nil // undeleting not deleted block is not considered an error (for now) } @@ -544,14 +496,14 @@ func (s *SQLStore) undeleteBlock(db sq.BaseRunner, c store.Container, blockID st now := utils.GetMillis() columns := []string{ - "workspace_id", + "board_id", + "channel_id", "id", "parent_id", s.escapeField("schema"), "type", "title", "fields", - "root_id", "modified_by", "create_at", "update_at", @@ -560,14 +512,14 @@ func (s *SQLStore) undeleteBlock(db sq.BaseRunner, c store.Container, blockID st } values := []interface{}{ - c.WorkspaceID, + block.BoardID, + "", block.ID, block.ParentID, block.Schema, block.Type, block.Title, fieldsJSON, - block.RootID, modifiedBy, block.CreateAt, now, @@ -591,6 +543,7 @@ func (s *SQLStore) undeleteBlock(db sq.BaseRunner, c store.Container, blockID st return nil } + func (s *SQLStore) getBlockCountsByType(db sq.BaseRunner) (map[string]int64, error) { query := s.getQueryBuilder(db). Select( @@ -624,16 +577,12 @@ func (s *SQLStore) getBlockCountsByType(db sq.BaseRunner) (map[string]int64, err return m, nil } -func (s *SQLStore) getBlock(db sq.BaseRunner, c store.Container, blockID string) (*model.Block, error) { +func (s *SQLStore) getBlock(db sq.BaseRunner, blockID string) (*model.Block, error) { query := s.getQueryBuilder(db). Select(s.blockFields()...). From(s.tablePrefix + "blocks"). Where(sq.Eq{"id": blockID}) - if c.WorkspaceID != "" { - query = query.Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}) - } - rows, err := query.Query() if err != nil { s.logger.Error(`GetBlock ERROR`, mlog.Err(err)) @@ -652,7 +601,7 @@ func (s *SQLStore) getBlock(db sq.BaseRunner, c store.Container, blockID string) return &blocks[0], nil } -func (s *SQLStore) getBlockHistory(db sq.BaseRunner, c store.Container, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) { +func (s *SQLStore) getBlockHistory(db sq.BaseRunner, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) { var order string if opts.Descending { order = " DESC " @@ -662,8 +611,7 @@ func (s *SQLStore) getBlockHistory(db sq.BaseRunner, c store.Container, blockID Select(s.blockFields()...). From(s.tablePrefix + "blocks_history"). Where(sq.Eq{"id": blockID}). - Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID}). - OrderBy("insert_at" + order) + OrderBy("insert_at " + order + ", update_at" + order) if opts.BeforeUpdateAt != 0 { query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt}) @@ -688,14 +636,14 @@ func (s *SQLStore) getBlockHistory(db sq.BaseRunner, c store.Container, blockID // getBoardAndCardByID returns the first parent of type `card` and first parent of type `board` for the block specified by ID. // `board` and/or `card` may return nil without error if the block does not belong to a board or card. -func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, c store.Container, blockID string) (board *model.Block, card *model.Block, err error) { +func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, blockID string) (board *model.Board, card *model.Block, err error) { // use block_history to fetch block in case it was deleted and no longer exists in blocks table. opts := model.QueryBlockHistoryOptions{ Limit: 1, Descending: true, } - blocks, err := s.getBlockHistory(db, c, blockID, opts) + blocks, err := s.getBlockHistory(db, blockID, opts) if err != nil { return nil, nil, err } @@ -704,12 +652,12 @@ func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, c store.Container, bloc return nil, nil, store.NewErrNotFound(blockID) } - return s.getBoardAndCard(db, c, &blocks[0]) + return s.getBoardAndCard(db, &blocks[0]) } -// getBoardAndCard returns the first parent of type `card` and first parent of type `board` for the specified block. +// getBoardAndCard returns the first parent of type `card` and and the `board` for the specified block. // `board` and/or `card` may return nil without error if the block does not belong to a board or card. -func (s *SQLStore) getBoardAndCard(db sq.BaseRunner, c store.Container, block *model.Block) (board *model.Block, card *model.Block, err error) { +func (s *SQLStore) getBoardAndCard(db sq.BaseRunner, block *model.Block) (board *model.Board, card *model.Block, err error) { var count int // don't let invalid blocks hierarchy cause infinite loop. iter := block @@ -721,50 +669,28 @@ func (s *SQLStore) getBoardAndCard(db sq.BaseRunner, c store.Container, block *m for { count++ - if board == nil && iter.Type == model.TypeBoard { - board = iter - } - if card == nil && iter.Type == model.TypeCard { card = iter } - if iter.ParentID == "" || (board != nil && card != nil) || count > maxSearchDepth { + if iter.ParentID == "" || card != nil || count > maxSearchDepth { break } - blocks, err := s.getBlockHistory(db, c, iter.ParentID, opts) - if err != nil { - return nil, nil, err + blocks, err2 := s.getBlockHistory(db, iter.ParentID, opts) + if err2 != nil { + return nil, nil, err2 } if len(blocks) == 0 { return board, card, nil } iter = &blocks[0] } - return board, card, nil -} - -func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]model.Block, error) { - subquery, _, _ := s.getQueryBuilder(db). - Select("id"). - From(s.tablePrefix + "blocks"). - Having("count(id) > 1"). - GroupBy("id"). - ToSql() - - rows, err := s.getQueryBuilder(db). - Select(s.blockFields()...). - From(s.tablePrefix + "blocks"). - Where(fmt.Sprintf("id IN (%s)", subquery)). - Query() + board, err = s.getBoard(db, block.BoardID) if err != nil { - s.logger.Error(`getBlocksWithSameID ERROR`, mlog.Err(err)) - return nil, err + return nil, nil, err } - defer s.CloseRows(rows) - - return s.blocksFromRows(rows) + return board, card, nil } func (s *SQLStore) replaceBlockID(db sq.BaseRunner, currentID, newID, workspaceID string) error { @@ -793,14 +719,14 @@ func (s *SQLStore) replaceBlockID(db sq.BaseRunner, currentID, newID, workspaceI return errID } - // update RootID - updateRootIDQ := baseQuery.Update(""). - Set("root_id", newID). - Where(sq.Eq{"root_id": currentID}) + // update BoardID + updateBoardIDQ := baseQuery.Update(""). + Set("board_id", newID). + Where(sq.Eq{"board_id": currentID}) - if errRootID := runUpdateForBlocksAndHistory(updateRootIDQ); errRootID != nil { - s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errRootID)) - return errRootID + if errBoardID := runUpdateForBlocksAndHistory(updateBoardIDQ); errBoardID != nil { + s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errBoardID)) + return errBoardID } // update ParentID @@ -815,7 +741,7 @@ func (s *SQLStore) replaceBlockID(db sq.BaseRunner, currentID, newID, workspaceI // update parent contentOrder updateContentOrder := baseQuery.Update("") - if s.dbType == postgresDBType { + if s.dbType == model.PostgresDBType { updateContentOrder = updateContentOrder. Set("fields", sq.Expr("REPLACE(fields::text, ?, ?)::json", currentID, newID)). Where(sq.Like{"fields->>'contentOrder'": "%" + currentID + "%"}). @@ -834,3 +760,34 @@ func (s *SQLStore) replaceBlockID(db sq.BaseRunner, currentID, newID, workspaceI return nil } + +func (s *SQLStore) duplicateBlock(db sq.BaseRunner, boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) { + blocks, err := s.getSubTree3(db, boardID, blockID, model.QuerySubtreeOptions{}) + if err != nil { + return nil, err + } + if len(blocks) == 0 { + return nil, BlockNotFoundErr{blockID} + } + + var rootBlock model.Block + allBlocks := []model.Block{} + for _, block := range blocks { + if block.ID == blockID { + if block.Fields == nil { + block.Fields = make(map[string]interface{}) + } + block.Fields["isTemplate"] = asTemplate + rootBlock = block + } else { + allBlocks = append(allBlocks, block) + } + } + allBlocks = append([]model.Block{rootBlock}, allBlocks...) + + allBlocks = model.GenerateBlockIDs(allBlocks, nil) + if err := s.insertBlocks(db, allBlocks, userID); err != nil { + return nil, err + } + return allBlocks, nil +} diff --git a/server/services/store/sqlstore/board.go b/server/services/store/sqlstore/board.go new file mode 100644 index 000000000..421fab52c --- /dev/null +++ b/server/services/store/sqlstore/board.go @@ -0,0 +1,530 @@ +package sqlstore + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/mattermost/focalboard/server/utils" + + sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +type BoardNotFoundErr struct { + boardID string +} + +func (be BoardNotFoundErr) Error() string { + return fmt.Sprintf("board not found (board id: %s", be.boardID) +} + +func boardFields(prefix string) []string { + fields := []string{ + "id", + "team_id", + "channel_id", + "created_by", + "modified_by", + "type", + "title", + "description", + "icon", + "show_description", + "is_template", + "template_version", + "COALESCE(properties, '{}')", + "COALESCE(card_properties, '{}')", + "COALESCE(column_calculations, '{}')", + "create_at", + "update_at", + "delete_at", + } + + if prefix == "" { + return fields + } + + prefixedFields := make([]string, len(fields)) + for i, field := range fields { + if strings.HasPrefix(field, "COALESCE(") { + prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1) + } else { + prefixedFields[i] = prefix + field + } + } + return prefixedFields +} + +var boardMemberFields = []string{ + "board_id", + "user_id", + "roles", + "scheme_admin", + "scheme_editor", + "scheme_commenter", + "scheme_viewer", +} + +func (s *SQLStore) boardsFromRows(rows *sql.Rows) ([]*model.Board, error) { + boards := []*model.Board{} + + for rows.Next() { + var board model.Board + var propertiesBytes []byte + var cardPropertiesBytes []byte + var columnCalculationsBytes []byte + + err := rows.Scan( + &board.ID, + &board.TeamID, + &board.ChannelID, + &board.CreatedBy, + &board.ModifiedBy, + &board.Type, + &board.Title, + &board.Description, + &board.Icon, + &board.ShowDescription, + &board.IsTemplate, + &board.TemplateVersion, + &propertiesBytes, + &cardPropertiesBytes, + &columnCalculationsBytes, + &board.CreateAt, + &board.UpdateAt, + &board.DeleteAt, + ) + if err != nil { + s.logger.Error("boardsFromRows scan error", mlog.Err(err)) + return nil, err + } + + err = json.Unmarshal(propertiesBytes, &board.Properties) + if err != nil { + s.logger.Error("board properties unmarshal error", mlog.Err(err)) + return nil, err + } + err = json.Unmarshal(cardPropertiesBytes, &board.CardProperties) + if err != nil { + s.logger.Error("board card properties unmarshal error", mlog.Err(err)) + return nil, err + } + err = json.Unmarshal(columnCalculationsBytes, &board.ColumnCalculations) + if err != nil { + s.logger.Error("board column calculation unmarshal error", mlog.Err(err)) + return nil, err + } + + boards = append(boards, &board) + } + + return boards, nil +} + +func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, error) { + boardMembers := []*model.BoardMember{} + + for rows.Next() { + var boardMember model.BoardMember + + err := rows.Scan( + &boardMember.BoardID, + &boardMember.UserID, + &boardMember.Roles, + &boardMember.SchemeAdmin, + &boardMember.SchemeEditor, + &boardMember.SchemeCommenter, + &boardMember.SchemeViewer, + ) + if err != nil { + return nil, err + } + + boardMembers = append(boardMembers, &boardMember) + } + + return boardMembers, nil +} + +func (s *SQLStore) getBoardByCondition(db sq.BaseRunner, conditions ...interface{}) (*model.Board, error) { + boards, err := s.getBoardsByCondition(db, conditions...) + if err != nil { + return nil, err + } + + return boards[0], nil +} + +func (s *SQLStore) getBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) { + query := s.getQueryBuilder(db). + Select(boardFields("")...). + From(s.tablePrefix + "boards") + for _, c := range conditions { + query = query.Where(c) + } + + rows, err := query.Query() + if err != nil { + s.logger.Error(`getBoardsByCondition ERROR`, mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + boards, err := s.boardsFromRows(rows) + if err != nil { + return nil, err + } + + if len(boards) == 0 { + return nil, sql.ErrNoRows + } + + return boards, nil +} + +func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, error) { + return s.getBoardByCondition(db, sq.Eq{"id": boardID}) +} + +func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) { + query := s.getQueryBuilder(db). + Select(boardFields("b.")...). + From(s.tablePrefix + "boards as b"). + Join(s.tablePrefix + "board_members as bm on b.id=bm.board_id"). + Where(sq.Eq{"b.team_id": teamID}). + Where(sq.Eq{"bm.user_id": userID}). + Where(sq.Eq{"b.is_template": false}) + + rows, err := query.Query() + if err != nil { + s.logger.Error(`getBoardsForUserAndTeam ERROR`, mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + return s.boardsFromRows(rows) +} + +func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) { + propertiesBytes, err := json.Marshal(board.Properties) + if err != nil { + return nil, err + } + cardPropertiesBytes, err := json.Marshal(board.CardProperties) + if err != nil { + return nil, err + } + columnCalculationsBytes, err := json.Marshal(board.ColumnCalculations) + if err != nil { + return nil, err + } + + existingBoard, err := s.getBoard(db, board.ID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, err + } + + insertQuery := s.getQueryBuilder(db).Insert(""). + Columns(boardFields("")...) + + insertQueryValues := map[string]interface{}{ + "id": board.ID, + "team_id": board.TeamID, + "channel_id": board.ChannelID, + "created_by": board.CreatedBy, + "modified_by": userID, + "type": board.Type, + "title": board.Title, + "description": board.Description, + "icon": board.Icon, + "show_description": board.ShowDescription, + "is_template": board.IsTemplate, + "template_version": board.TemplateVersion, + "properties": propertiesBytes, + "card_properties": cardPropertiesBytes, + "column_calculations": columnCalculationsBytes, + "create_at": board.CreateAt, + "update_at": board.UpdateAt, + "delete_at": board.DeleteAt, + } + + now := utils.GetMillis() + + if existingBoard != nil { + query := s.getQueryBuilder(db).Update(s.tablePrefix+"boards"). + Where(sq.Eq{"id": board.ID}). + Set("modified_by", userID). + Set("type", board.Type). + Set("title", board.Title). + Set("description", board.Description). + Set("icon", board.Icon). + Set("show_description", board.ShowDescription). + Set("is_template", board.IsTemplate). + Set("template_version", board.TemplateVersion). + Set("properties", propertiesBytes). + Set("card_properties", cardPropertiesBytes). + Set("column_calculations", columnCalculationsBytes). + Set("update_at", now). + Set("delete_at", board.DeleteAt) + + if _, err := query.Exec(); err != nil { + s.logger.Error(`InsertBoard error occurred while updating existing board`, mlog.String("boardID", board.ID), mlog.Err(err)) + return nil, err + } + } else { + insertQueryValues["created_by"] = userID + insertQueryValues["create_at"] = now + insertQueryValues["update_at"] = now + + query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards") + if _, err := query.Exec(); err != nil { + return nil, err + } + } + + // writing board history + query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history") + if _, err := query.Exec(); err != nil { + return nil, err + } + + return s.getBoard(db, board.ID) +} + +func (s *SQLStore) patchBoard(db sq.BaseRunner, boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) { + existingBoard, err := s.getBoard(db, boardID) + if err != nil { + return nil, err + } + if existingBoard == nil { + return nil, BoardNotFoundErr{boardID} + } + + board := boardPatch.Patch(existingBoard) + return s.insertBoard(db, board, userID) +} + +func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error { + now := utils.GetMillis() + + board, err := s.getBoard(db, boardID) + if err != nil { + fmt.Printf("error on get board: %s\n", err) + return err + } + + insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix+"boards_history"). + Columns( + "team_id", + "id", + "type", + "modified_by", + "update_at", + "delete_at", + ). + Values( + board.TeamID, + boardID, + board.Type, + userID, + now, + now, + ) + + if _, err := insertQuery.Exec(); err != nil { + return err + } + + deleteQuery := s.getQueryBuilder(db). + Delete(s.tablePrefix + "boards"). + Where(sq.Eq{"id": boardID}). + Where(sq.Eq{"COALESCE(team_id, '0')": board.TeamID}) + + if _, err := deleteQuery.Exec(); err != nil { + return err + } + + return nil +} + +func (s *SQLStore) insertBoardWithAdmin(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, *model.BoardMember, error) { + newBoard, err := s.insertBoard(db, board, userID) + if err != nil { + return nil, nil, err + } + + bm := &model.BoardMember{ + BoardID: newBoard.ID, + UserID: newBoard.CreatedBy, + SchemeAdmin: true, + SchemeEditor: true, + } + + nbm, err := s.saveMember(db, bm) + if err != nil { + return nil, nil, err + } + + return newBoard, nbm, nil +} + +func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.BoardMember, error) { + queryValues := map[string]interface{}{ + "board_id": bm.BoardID, + "user_id": bm.UserID, + "roles": "", + "scheme_admin": bm.SchemeAdmin, + "scheme_editor": bm.SchemeEditor, + "scheme_commenter": bm.SchemeCommenter, + "scheme_viewer": bm.SchemeViewer, + } + + query := s.getQueryBuilder(db). + Insert(s.tablePrefix + "board_members"). + SetMap(queryValues) + + if s.dbType == model.MysqlDBType { + query = query.Suffix( + "ON DUPLICATE KEY UPDATE scheme_admin = ?, scheme_editor = ?, scheme_commenter = ?, scheme_viewer = ?", + bm.SchemeAdmin, bm.SchemeEditor, bm.SchemeCommenter, bm.SchemeViewer) + } else { + query = query.Suffix( + `ON CONFLICT (board_id, user_id) + DO UPDATE SET scheme_admin = EXCLUDED.scheme_admin, scheme_editor = EXCLUDED.scheme_editor, + scheme_commenter = EXCLUDED.scheme_commenter, scheme_viewer = EXCLUDED.scheme_viewer`, + ) + } + + if _, err := query.Exec(); err != nil { + return nil, err + } + + return bm, nil +} + +func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error { + deleteQuery := s.getQueryBuilder(db). + Delete(s.tablePrefix + "board_members"). + Where(sq.Eq{"board_id": boardID}). + Where(sq.Eq{"user_id": userID}) + + if _, err := deleteQuery.Exec(); err != nil { + return err + } + + return nil +} + +func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (*model.BoardMember, error) { + query := s.getQueryBuilder(db). + Select(boardMemberFields...). + From(s.tablePrefix + "board_members"). + Where(sq.Eq{"board_id": boardID}). + Where(sq.Eq{"user_id": userID}) + + rows, err := query.Query() + if err != nil { + s.logger.Error(`getMemberForBoard ERROR`, mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + members, err := s.boardMembersFromRows(rows) + if err != nil { + return nil, err + } + + if len(members) == 0 { + return nil, sql.ErrNoRows + } + + return members[0], nil +} + +func (s *SQLStore) getMembersForUser(db sq.BaseRunner, userID string) ([]*model.BoardMember, error) { + query := s.getQueryBuilder(db). + Select(boardMemberFields...). + From(s.tablePrefix + "board_members"). + Where(sq.Eq{"user_id": userID}) + + rows, err := query.Query() + if err != nil { + s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + members, err := s.boardMembersFromRows(rows) + if err != nil { + return nil, err + } + + return members, nil +} + +func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*model.BoardMember, error) { + query := s.getQueryBuilder(db). + Select(boardMemberFields...). + From(s.tablePrefix + "board_members"). + Where(sq.Eq{"board_id": boardID}) + + rows, err := query.Query() + if err != nil { + s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + return s.boardMembersFromRows(rows) +} + +// searchBoardsForUserAndTeam returns all boards that match with the +// term that are either private and which the user is a member of, or +// they're open, regardless of the user membership. +// Search is case-insensitive. +func (s *SQLStore) searchBoardsForUserAndTeam(db sq.BaseRunner, term, userID, teamID string) ([]*model.Board, error) { + query := s.getQueryBuilder(db). + Select(boardFields("b.")...). + Distinct(). + From(s.tablePrefix + "boards as b"). + LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id"). + Where(sq.Eq{"b.team_id": teamID}). + Where(sq.Eq{"b.is_template": false}). + Where(sq.Or{ + sq.Eq{"b.type": model.BoardTypeOpen}, + sq.And{ + sq.Eq{"b.type": model.BoardTypePrivate}, + sq.Eq{"bm.user_id": userID}, + }, + }) + + if term != "" { + // break search query into space separated words + // and search for each word. + // This should later be upgraded to industrial-strength + // word tokenizer, that uses much more than space + // to break words. + + conditions := sq.Or{} + + for _, word := range strings.Split(strings.TrimSpace(term), " ") { + conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"}) + } + + query = query.Where(conditions) + } + + rows, err := query.Query() + if err != nil { + s.logger.Error(`searchBoardsForUserAndTeam ERROR`, mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + return s.boardsFromRows(rows) +} diff --git a/server/services/store/sqlstore/boards_and_blocks.go b/server/services/store/sqlstore/boards_and_blocks.go new file mode 100644 index 000000000..37132aabf --- /dev/null +++ b/server/services/store/sqlstore/boards_and_blocks.go @@ -0,0 +1,164 @@ +package sqlstore + +import ( + "database/sql" + "fmt" + + sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" +) + +type BlockDoesntBelongToBoardsErr struct { + blockID string +} + +func (e BlockDoesntBelongToBoardsErr) Error() string { + return fmt.Sprintf("block %s doesn't belong to any of the boards in the delete request", e.blockID) +} + +func (s *SQLStore) createBoardsAndBlocksWithAdmin(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) { + newBab, err := s.createBoardsAndBlocks(db, bab, userID) + if err != nil { + return nil, nil, err + } + + members := []*model.BoardMember{} + for _, board := range newBab.Boards { + bm := &model.BoardMember{ + BoardID: board.ID, + UserID: board.CreatedBy, + SchemeAdmin: true, + SchemeEditor: true, + } + + nbm, err := s.saveMember(db, bm) + if err != nil { + return nil, nil, err + } + + members = append(members, nbm) + } + + return newBab, members, nil +} + +func (s *SQLStore) createBoardsAndBlocks(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { + boards := []*model.Board{} + blocks := []model.Block{} + + for _, board := range bab.Boards { + newBoard, err := s.insertBoard(db, board, userID) + if err != nil { + return nil, err + } + + boards = append(boards, newBoard) + } + + for _, block := range bab.Blocks { + b := block + err := s.insertBlock(db, &b, userID) + if err != nil { + return nil, err + } + + blocks = append(blocks, block) + } + + newBab := &model.BoardsAndBlocks{ + Boards: boards, + Blocks: blocks, + } + + return newBab, nil +} + +func (s *SQLStore) patchBoardsAndBlocks(db sq.BaseRunner, pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { + bab := &model.BoardsAndBlocks{} + for i, boardID := range pbab.BoardIDs { + board, err := s.patchBoard(db, boardID, pbab.BoardPatches[i], userID) + if err != nil { + return nil, err + } + bab.Boards = append(bab.Boards, board) + } + + for i, blockID := range pbab.BlockIDs { + if err := s.patchBlock(db, blockID, pbab.BlockPatches[i], userID); err != nil { + return nil, err + } + block, err := s.getBlock(db, blockID) + if err != nil { + return nil, err + } + bab.Blocks = append(bab.Blocks, *block) + } + + return bab, nil +} + +// deleteBoardsAndBlocks deletes all the boards and blocks entities of +// the DeleteBoardsAndBlocks struct, making sure that all the blocks +// belong to the boards in the struct. +func (s *SQLStore) deleteBoardsAndBlocks(db sq.BaseRunner, dbab *model.DeleteBoardsAndBlocks, userID string) error { + boardIDMap := map[string]bool{} + for _, boardID := range dbab.Boards { + if err := s.deleteBoard(db, boardID, userID); err != nil { + return err + } + + boardIDMap[boardID] = true + } + + for _, blockID := range dbab.Blocks { + block, err := s.getBlock(db, blockID) + if err != nil { + return err + } + if block == nil { + return sql.ErrNoRows + } + + if _, ok := boardIDMap[block.BoardID]; !ok { + return BlockDoesntBelongToBoardsErr{blockID} + } + + if err := s.deleteBlock(db, blockID, userID); err != nil { + return err + } + } + + return nil +} + +func (s *SQLStore) duplicateBoard(db sq.BaseRunner, boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { + bab := &model.BoardsAndBlocks{ + Boards: []*model.Board{}, + Blocks: []model.Block{}, + } + + board, err := s.getBoard(db, boardID) + if err != nil { + return nil, nil, err + } + board.IsTemplate = asTemplate + board.CreatedBy = userID + + if toTeam != "" { + board.TeamID = toTeam + } + + bab.Boards = []*model.Board{board} + blocks, err := s.getBlocksWithBoardID(db, boardID) + if err != nil { + return nil, nil, err + } + bab.Blocks = blocks + + bab, err = model.GenerateBoardsAndBlocksIDs(bab, nil) + if err != nil { + return nil, nil, err + } + + return s.createBoardsAndBlocksWithAdmin(db, bab, userID) +} diff --git a/server/services/store/sqlstore/category.go b/server/services/store/sqlstore/category.go new file mode 100644 index 000000000..44d4d71da --- /dev/null +++ b/server/services/store/sqlstore/category.go @@ -0,0 +1,151 @@ +package sqlstore + +import ( + "database/sql" + + sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/store" + "github.com/mattermost/focalboard/server/utils" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +func (s *SQLStore) getCategory(db sq.BaseRunner, id string) (*model.Category, error) { + query := s.getQueryBuilder(db). + Select("id", "name", "user_id", "team_id", "create_at", "update_at", "delete_at"). + From(s.tablePrefix + "categories"). + Where(sq.Eq{"id": id}) + + rows, err := query.Query() + if err != nil { + s.logger.Error("getCategory error", mlog.Err(err)) + return nil, err + } + + categories, err := s.categoriesFromRows(rows) + if err != nil { + s.logger.Error("getCategory row scan error", mlog.Err(err)) + return nil, err + } + + if len(categories) == 0 { + return nil, store.NewErrNotFound(id) + } + + return &categories[0], nil +} + +func (s *SQLStore) createCategory(db sq.BaseRunner, category model.Category) error { + query := s.getQueryBuilder(db). + Insert(s.tablePrefix+"categories"). + Columns( + "id", + "name", + "user_id", + "team_id", + "create_at", + "update_at", + "delete_at", + ). + Values( + category.ID, + category.Name, + category.UserID, + category.TeamID, + category.CreateAt, + category.UpdateAt, + category.DeleteAt, + ) + + _, err := query.Exec() + if err != nil { + s.logger.Error("Error creating category", mlog.String("category name", category.Name), mlog.Err(err)) + return err + } + return nil +} + +func (s *SQLStore) updateCategory(db sq.BaseRunner, category model.Category) error { + query := s.getQueryBuilder(db). + Update(s.tablePrefix+"categories"). + Set("name", category.Name). + Set("update_at", category.UpdateAt). + Where(sq.Eq{"id": category.ID}) + + _, err := query.Exec() + if err != nil { + s.logger.Error("Error updating category", mlog.String("category_id", category.ID), mlog.String("category_name", category.Name), mlog.Err(err)) + return err + } + return nil +} + +func (s *SQLStore) deleteCategory(db sq.BaseRunner, categoryID, userID, teamID string) error { + query := s.getQueryBuilder(db). + Update(s.tablePrefix+"categories"). + Set("delete_at", utils.GetMillis()). + Where(sq.Eq{ + "id": categoryID, + "user_id": userID, + "team_id": teamID, + }) + + _, err := query.Exec() + if err != nil { + s.logger.Error( + "Error updating category", + mlog.String("category_id", categoryID), + mlog.String("user_id", userID), + mlog.String("team_id", teamID), + mlog.Err(err), + ) + return err + } + return nil +} + +func (s *SQLStore) getUserCategories(db sq.BaseRunner, userID, teamID string) ([]model.Category, error) { + query := s.getQueryBuilder(db). + Select("id", "name", "user_id", "team_id", "create_at", "update_at", "delete_at"). + From(s.tablePrefix + "categories"). + Where(sq.Eq{ + "user_id": userID, + "team_id": teamID, + "delete_at": 0, + }) + + rows, err := query.Query() + if err != nil { + s.logger.Error("getUserCategories error", mlog.Err(err)) + return nil, err + } + + return s.categoriesFromRows(rows) +} + +func (s *SQLStore) categoriesFromRows(rows *sql.Rows) ([]model.Category, error) { + var categories []model.Category + + for rows.Next() { + category := model.Category{} + err := rows.Scan( + &category.ID, + &category.Name, + &category.UserID, + &category.TeamID, + &category.CreateAt, + &category.UpdateAt, + &category.DeleteAt, + ) + + if err != nil { + s.logger.Error("categoriesFromRows row parsing error", mlog.Err(err)) + return nil, err + } + + categories = append(categories, category) + } + + return categories, nil +} diff --git a/server/services/store/sqlstore/category_boards.go b/server/services/store/sqlstore/category_boards.go new file mode 100644 index 000000000..91dc4e727 --- /dev/null +++ b/server/services/store/sqlstore/category_boards.go @@ -0,0 +1,196 @@ +package sqlstore + +import ( + "database/sql" + "errors" + + sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/utils" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +var ( + errDuplicateCategoryEntries = errors.New("duplicate entries found for user-board-category mapping") +) + +func (s *SQLStore) getUserCategoryBlocks(db sq.BaseRunner, userID, teamID string) ([]model.CategoryBlocks, error) { + categories, err := s.getUserCategories(db, userID, teamID) + if err != nil { + return nil, err + } + + userCategoryBlocks := []model.CategoryBlocks{} + for _, category := range categories { + blockIDs, err := s.getCategoryBlockAttributes(db, category.ID) + if err != nil { + return nil, err + } + + userCategoryBlock := model.CategoryBlocks{ + Category: category, + BlockIDs: blockIDs, + } + + userCategoryBlocks = append(userCategoryBlocks, userCategoryBlock) + } + + return userCategoryBlocks, nil +} + +func (s *SQLStore) getCategoryBlockAttributes(db sq.BaseRunner, categoryID string) ([]string, error) { + query := s.getQueryBuilder(db). + Select("block_id"). + From(s.tablePrefix + "category_blocks"). + Where(sq.Eq{ + "category_id": categoryID, + "delete_at": 0, + }) + + rows, err := query.Query() + if err != nil { + s.logger.Error("getCategoryBlocks error fetching categoryblocks", mlog.String("categoryID", categoryID), mlog.Err(err)) + return nil, err + } + + return s.categoryBlocksFromRows(rows) +} + +func (s *SQLStore) addUpdateCategoryBlock(db sq.BaseRunner, userID, categoryID, blockID string) error { + if categoryID == "0" { + return s.deleteUserCategoryBlock(db, userID, blockID) + } + + rowsAffected, err := s.updateUserCategoryBlock(db, userID, blockID, categoryID) + if err != nil { + return err + } + + if rowsAffected > 1 { + return errDuplicateCategoryEntries + } + + if rowsAffected == 0 { + // user-block mapping didn't already exist. So we'll create a new entry + return s.addUserCategoryBlock(db, userID, categoryID, blockID) + } + + return nil +} + +/* +func (s *SQLStore) userCategoryBlockExists(db sq.BaseRunner, userID, teamID, categoryID, blockID string) (bool, error) { + query := s.getQueryBuilder(db). + Select("blocks.id"). + From(s.tablePrefix + "categories AS categories"). + Join(s.tablePrefix + "category_blocks AS blocks ON blocks.category_id = categories.id"). + Where(sq.Eq{ + "user_id": userID, + "team_id": teamID, + "categories.id": categoryID, + "block_id": blockID, + }) + + rows, err := query.Query() + if err != nil { + s.logger.Error("getCategoryBlock error", mlog.Err(err)) + return false, err + } + + return rows.Next(), nil +} +*/ + +func (s *SQLStore) updateUserCategoryBlock(db sq.BaseRunner, userID, blockID, categoryID string) (int64, error) { + result, err := s.getQueryBuilder(db). + Update(s.tablePrefix+"category_blocks"). + Set("category_id", categoryID). + Set("delete_at", 0). + Where(sq.Eq{ + "block_id": blockID, + "user_id": userID, + }). + Exec() + + if err != nil { + s.logger.Error("updateUserCategoryBlock error", mlog.Err(err)) + return 0, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + s.logger.Error("updateUserCategoryBlock affected row count error", mlog.Err(err)) + return 0, err + } + + return rowsAffected, nil +} + +func (s *SQLStore) addUserCategoryBlock(db sq.BaseRunner, userID, categoryID, blockID string) error { + _, err := s.getQueryBuilder(db). + Insert(s.tablePrefix+"category_blocks"). + Columns( + "id", + "user_id", + "category_id", + "block_id", + "create_at", + "update_at", + "delete_at", + ). + Values( + utils.NewID(utils.IDTypeNone), + userID, + categoryID, + blockID, + utils.GetMillis(), + utils.GetMillis(), + 0, + ).Exec() + + if err != nil { + s.logger.Error("addUserCategoryBlock error", mlog.Err(err)) + return err + } + return nil +} + +func (s *SQLStore) deleteUserCategoryBlock(db sq.BaseRunner, userID, blockID string) error { + _, err := s.getQueryBuilder(db). + Update(s.tablePrefix+"category_blocks"). + Set("delete_at", utils.GetMillis()). + Where(sq.Eq{ + "user_id": userID, + "block_id": blockID, + "delete_at": 0, + }).Exec() + + if err != nil { + s.logger.Error( + "deleteUserCategoryBlock delete error", + mlog.String("userID", userID), + mlog.String("blockID", blockID), + mlog.Err(err), + ) + return err + } + + return nil +} + +func (s *SQLStore) categoryBlocksFromRows(rows *sql.Rows) ([]string, error) { + blocks := []string{} + + for rows.Next() { + blockID := "" + if err := rows.Scan(&blockID); err != nil { + s.logger.Error("categoryBlocksFromRows row scan error", mlog.Err(err)) + return nil, err + } + + blocks = append(blocks, blockID) + } + + return blocks, nil +} diff --git a/server/services/store/sqlstore/data_migrations.go b/server/services/store/sqlstore/data_migrations.go index 4678db34a..8317e4273 100644 --- a/server/services/store/sqlstore/data_migrations.go +++ b/server/services/store/sqlstore/data_migrations.go @@ -5,15 +5,61 @@ import ( "fmt" "strconv" + sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/utils" + "github.com/mattermost/mattermost-server/v6/shared/mlog" ) const ( - UniqueIDsMigrationKey = "UniqueIDsMigrationComplete" + TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete" + UniqueIDsMigrationKey = "UniqueIDsMigrationComplete" + CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete" + + categoriesUUIDIDMigrationRequiredVersion = 19 ) +func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]model.Block, error) { + subquery, _, _ := s.getQueryBuilder(db). + Select("id"). + From(s.tablePrefix + "blocks"). + Having("count(id) > 1"). + GroupBy("id"). + ToSql() + + blocksFields := []string{ + "id", + "parent_id", + "root_id", + "created_by", + "modified_by", + s.escapeField("schema"), + "type", + "title", + "COALESCE(fields, '{}')", + s.timestampToCharField("insert_at", "insertAt"), + "create_at", + "update_at", + "delete_at", + "COALESCE(workspace_id, '0')", + } + + rows, err := s.getQueryBuilder(db). + Select(blocksFields...). + From(s.tablePrefix + "blocks"). + Where(fmt.Sprintf("id IN (%s)", subquery)). + Query() + if err != nil { + s.logger.Error(`getBlocksWithSameID ERROR`, mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + return s.blocksFromRows(rows) +} + func (s *SQLStore) runUniqueIDsMigration() error { setting, err := s.GetSystemSetting(UniqueIDsMigrationKey) if err != nil { @@ -35,7 +81,7 @@ func (s *SQLStore) runUniqueIDsMigration() error { blocks, err := s.getBlocksWithSameID(tx) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "getBlocksWithSameID")) + s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "getBlocksWithSameID")) } return fmt.Errorf("cannot get blocks with same ID: %w", err) } @@ -55,7 +101,7 @@ func (s *SQLStore) runUniqueIDsMigration() error { newID := utils.NewID(model.BlockType2IDType(block.Type)) if err := s.replaceBlockID(tx, block.ID, newID, block.WorkspaceID); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "replaceBlockID")) + s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "replaceBlockID")) } return fmt.Errorf("cannot replace blockID %s: %w", block.ID, err) } @@ -64,7 +110,7 @@ func (s *SQLStore) runUniqueIDsMigration() error { if err := s.setSystemSetting(tx, UniqueIDsMigrationKey, strconv.FormatBool(true)); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting")) + s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting")) } return fmt.Errorf("cannot mark migration as completed: %w", err) } @@ -76,3 +122,178 @@ func (s *SQLStore) runUniqueIDsMigration() error { s.logger.Debug("Unique IDs migration finished successfully") return nil } + +func (s *SQLStore) runCategoryUUIDIDMigration() error { + setting, err := s.GetSystemSetting(CategoryUUIDIDMigrationKey) + if err != nil { + return fmt.Errorf("cannot get migration state: %w", err) + } + + // If the migration is already completed, do not run it again. + if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun { + return nil + } + + s.logger.Debug("Running category UUID ID migration") + + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return txErr + } + + if err := s.updateCategoryIDs(tx); err != nil { + return err + } + + if err := s.updateCategoryBlocksIDs(tx); err != nil { + return err + } + + if err := s.setSystemSetting(tx, CategoryUUIDIDMigrationKey, strconv.FormatBool(true)); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("category IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting")) + } + return fmt.Errorf("cannot mark migration as completed: %w", err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("cannot commit category IDs transaction: %w", err) + } + + s.logger.Debug("category IDs migration finished successfully") + return nil +} + +func (s *SQLStore) updateCategoryIDs(db sq.BaseRunner) error { + // fetch all category IDs + oldCategoryIDs, err := s.getIDs(db, "categories") + if err != nil { + return err + } + + // map old category ID to new ID + categoryIDs := map[string]string{} + for _, oldID := range oldCategoryIDs { + newID := utils.NewID(utils.IDTypeNone) + categoryIDs[oldID] = newID + } + + // update for each category ID. + // Update the new ID in category table, + // and update corresponding rows in category boards table. + for oldID, newID := range categoryIDs { + if err := s.updateCategoryID(db, oldID, newID); err != nil { + return err + } + } + + return nil +} + +func (s *SQLStore) getIDs(db sq.BaseRunner, table string) ([]string, error) { + rows, err := s.getQueryBuilder(db). + Select("id"). + From(s.tablePrefix + table). + Query() + + if err != nil { + s.logger.Error("getIDs error", mlog.String("table", table), mlog.Err(err)) + return nil, err + } + + defer s.CloseRows(rows) + var categoryIDs []string + for rows.Next() { + var id string + err := rows.Scan(&id) + if err != nil { + s.logger.Error("getIDs scan row error", mlog.String("table", table), mlog.Err(err)) + return nil, err + } + + categoryIDs = append(categoryIDs, id) + } + + return categoryIDs, nil +} + +func (s *SQLStore) updateCategoryID(db sq.BaseRunner, oldID, newID string) error { + // update in category table + rows, err := s.getQueryBuilder(db). + Update(s.tablePrefix+"categories"). + Set("id", newID). + Where(sq.Eq{"id": oldID}). + Query() + + if err != nil { + s.logger.Error("updateCategoryID update category error", mlog.Err(err)) + return err + } + + if err = rows.Close(); err != nil { + s.logger.Error("updateCategoryID error closing rows after updating categories table IDs", mlog.Err(err)) + return err + } + + // update category boards table + + rows, err = s.getQueryBuilder(db). + Update(s.tablePrefix+"category_blocks"). + Set("category_id", newID). + Where(sq.Eq{"category_id": oldID}). + Query() + + if err != nil { + s.logger.Error("updateCategoryID update category boards error", mlog.Err(err)) + return err + } + + if err := rows.Close(); err != nil { + s.logger.Error("updateCategoryID error closing rows after updating category boards table IDs", mlog.Err(err)) + return err + } + + return nil +} + +func (s *SQLStore) updateCategoryBlocksIDs(db sq.BaseRunner) error { + // fetch all category IDs + oldCategoryIDs, err := s.getIDs(db, "category_blocks") + if err != nil { + return err + } + + // map old category ID to new ID + categoryIDs := map[string]string{} + for _, oldID := range oldCategoryIDs { + newID := utils.NewID(utils.IDTypeNone) + categoryIDs[oldID] = newID + } + + // update for each category ID. + // Update the new ID in category table, + // and update corresponding rows in category boards table. + for oldID, newID := range categoryIDs { + if err := s.updateCategoryBlocksID(db, oldID, newID); err != nil { + return err + } + } + return nil +} + +func (s *SQLStore) updateCategoryBlocksID(db sq.BaseRunner, oldID, newID string) error { + // update in category table + rows, err := s.getQueryBuilder(db). + Update(s.tablePrefix+"category_blocks"). + Set("id", newID). + Where(sq.Eq{"id": oldID}). + Query() + + if err != nil { + s.logger.Error("updateCategoryBlocksID update category error", mlog.Err(err)) + return err + } + rows.Close() + + return nil +} diff --git a/server/services/store/sqlstore/data_migrations_test.go b/server/services/store/sqlstore/data_migrations_test.go index ff3eea1fa..0562313bf 100644 --- a/server/services/store/sqlstore/data_migrations_test.go +++ b/server/services/store/sqlstore/data_migrations_test.go @@ -5,46 +5,47 @@ import ( "time" "github.com/mattermost/focalboard/server/model" - st "github.com/mattermost/focalboard/server/services/store" "github.com/stretchr/testify/require" ) //nolint:gosec func TestGetBlocksWithSameID(t *testing.T) { + t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests") + store, tearDown := SetupTests(t) sqlStore := store.(*SQLStore) defer tearDown() - container1 := st.Container{WorkspaceID: "1"} - container2 := st.Container{WorkspaceID: "2"} - container3 := st.Container{WorkspaceID: "3"} + container1 := "1" + container2 := "2" + container3 := "3" - block1 := model.Block{ID: "block-id-1", RootID: "root-id-1"} - block2 := model.Block{ID: "block-id-2", RootID: "root-id-2"} - block3 := model.Block{ID: "block-id-3", RootID: "root-id-3"} + block1 := model.Block{ID: "block-id-1", BoardID: "board-id-1"} + block2 := model.Block{ID: "block-id-2", BoardID: "board-id-2"} + block3 := model.Block{ID: "block-id-3", BoardID: "board-id-3"} - block4 := model.Block{ID: "block-id-1", RootID: "root-id-1"} - block5 := model.Block{ID: "block-id-2", RootID: "root-id-2"} + block4 := model.Block{ID: "block-id-1", BoardID: "board-id-1"} + block5 := model.Block{ID: "block-id-2", BoardID: "board-id-2"} - block6 := model.Block{ID: "block-id-1", RootID: "root-id-1"} - block7 := model.Block{ID: "block-id-7", RootID: "root-id-7"} - block8 := model.Block{ID: "block-id-8", RootID: "root-id-8"} + block6 := model.Block{ID: "block-id-1", BoardID: "board-id-1"} + block7 := model.Block{ID: "block-id-7", BoardID: "board-id-7"} + block8 := model.Block{ID: "block-id-8", BoardID: "board-id-8"} for _, block := range []model.Block{block1, block2, block3} { - err := sqlStore.InsertBlock(container1, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container1, &block, "user-id") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } for _, block := range []model.Block{block4, block5} { - err := sqlStore.InsertBlock(container2, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container2, &block, "user-id") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } for _, block := range []model.Block{block6, block7, block8} { - err := sqlStore.InsertBlock(container3, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container3, &block, "user-id") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } @@ -58,7 +59,7 @@ func TestGetBlocksWithSameID(t *testing.T) { // able to compare both expected and found sets foundBlocks := []model.Block{} for _, foundBlock := range blocks { - foundBlocks = append(foundBlocks, model.Block{ID: foundBlock.ID, RootID: foundBlock.RootID}) + foundBlocks = append(foundBlocks, model.Block{ID: foundBlock.ID, BoardID: foundBlock.BoardID}) } require.ElementsMatch(t, blocksWithDuplicatedID, foundBlocks) @@ -66,41 +67,43 @@ func TestGetBlocksWithSameID(t *testing.T) { //nolint:gosec func TestReplaceBlockID(t *testing.T) { + t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests") + store, tearDown := SetupTests(t) sqlStore := store.(*SQLStore) defer tearDown() - container1 := st.Container{WorkspaceID: "1"} - container2 := st.Container{WorkspaceID: "2"} + container1 := "1" + container2 := "2" - // blocks from workspace1 - block1 := model.Block{ID: "block-id-1", RootID: "root-id-1"} - block2 := model.Block{ID: "block-id-2", RootID: "root-id-2", ParentID: "block-id-1"} - block3 := model.Block{ID: "block-id-3", RootID: "block-id-1"} - block4 := model.Block{ID: "block-id-4", RootID: "block-id-2"} - block5 := model.Block{ID: "block-id-5", RootID: "block-id-1", ParentID: "block-id-1"} + // blocks from team1 + block1 := model.Block{ID: "block-id-1", BoardID: "board-id-1"} + block2 := model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} + block3 := model.Block{ID: "block-id-3", BoardID: "block-id-1"} + block4 := model.Block{ID: "block-id-4", BoardID: "block-id-2"} + block5 := model.Block{ID: "block-id-5", BoardID: "block-id-1", ParentID: "block-id-1"} block8 := model.Block{ - ID: "block-id-8", RootID: "root-id-2", Type: model.TypeCard, + ID: "block-id-8", BoardID: "board-id-2", Type: model.TypeCard, Fields: map[string]interface{}{"contentOrder": []string{"block-id-1", "block-id-2"}}, } - // blocks from workspace2. They're identical to blocks 1 and 2, + // blocks from team2. They're identical to blocks 1 and 2, // but they shouldn't change - block6 := model.Block{ID: "block-id-1", RootID: "root-id-1"} - block7 := model.Block{ID: "block-id-2", RootID: "root-id-2", ParentID: "block-id-1"} + block6 := model.Block{ID: "block-id-1", BoardID: "board-id-1"} + block7 := model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} block9 := model.Block{ - ID: "block-id-8", RootID: "root-id-2", Type: model.TypeCard, + ID: "block-id-8", BoardID: "board-id-2", Type: model.TypeCard, Fields: map[string]interface{}{"contentOrder": []string{"block-id-1", "block-id-2"}}, } for _, block := range []model.Block{block1, block2, block3, block4, block5, block8} { - err := sqlStore.InsertBlock(container1, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container1, &block, "user-id") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } for _, block := range []model.Block{block6, block7, block9} { - err := sqlStore.InsertBlock(container2, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container2, &block, "user-id") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } @@ -110,27 +113,27 @@ func TestReplaceBlockID(t *testing.T) { err := sqlStore.replaceBlockID(sqlStore.db, currentID, newID, "1") require.NoError(t, err) - newBlock1, err := sqlStore.GetBlock(container1, newID) + newBlock1, err := sqlStore.getLegacyBlock(sqlStore.db, container1, newID) require.NoError(t, err) - newBlock2, err := sqlStore.GetBlock(container1, block2.ID) + newBlock2, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block2.ID) require.NoError(t, err) - newBlock3, err := sqlStore.GetBlock(container1, block3.ID) + newBlock3, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block3.ID) require.NoError(t, err) - newBlock5, err := sqlStore.GetBlock(container1, block5.ID) + newBlock5, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block5.ID) require.NoError(t, err) - newBlock6, err := sqlStore.GetBlock(container2, block6.ID) + newBlock6, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block6.ID) require.NoError(t, err) - newBlock7, err := sqlStore.GetBlock(container2, block7.ID) + newBlock7, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block7.ID) require.NoError(t, err) - newBlock8, err := sqlStore.GetBlock(container1, block8.ID) + newBlock8, err := sqlStore.GetBlock(block8.ID) require.NoError(t, err) - newBlock9, err := sqlStore.GetBlock(container2, block9.ID) + newBlock9, err := sqlStore.GetBlock(block9.ID) require.NoError(t, err) require.Equal(t, newID, newBlock1.ID) require.Equal(t, newID, newBlock2.ParentID) - require.Equal(t, newID, newBlock3.RootID) - require.Equal(t, newID, newBlock5.RootID) + require.Equal(t, newID, newBlock3.BoardID) + require.Equal(t, newID, newBlock5.BoardID) require.Equal(t, newID, newBlock5.ParentID) require.Equal(t, newBlock8.Fields["contentOrder"].([]interface{})[0], newID) require.Equal(t, newBlock8.Fields["contentOrder"].([]interface{})[1], "block-id-2") @@ -143,6 +146,8 @@ func TestReplaceBlockID(t *testing.T) { //nolint:gosec func TestRunUniqueIDsMigration(t *testing.T) { + t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests") + store, tearDown := SetupTests(t) sqlStore := store.(*SQLStore) defer tearDown() @@ -152,39 +157,39 @@ func TestRunUniqueIDsMigration(t *testing.T) { keyErr := sqlStore.SetSystemSetting(UniqueIDsMigrationKey, "false") require.NoError(t, keyErr) - container1 := st.Container{WorkspaceID: "1"} - container2 := st.Container{WorkspaceID: "2"} - container3 := st.Container{WorkspaceID: "3"} + container1 := "1" + container2 := "2" + container3 := "3" // blocks from workspace1. They shouldn't change, as the first // duplicated ID is preserved - block1 := model.Block{ID: "block-id-1", RootID: "root-id-1"} - block2 := model.Block{ID: "block-id-2", RootID: "root-id-2", ParentID: "block-id-1"} - block3 := model.Block{ID: "block-id-3", RootID: "block-id-1"} + block1 := model.Block{ID: "block-id-1", BoardID: "board-id-1"} + block2 := model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} + block3 := model.Block{ID: "block-id-3", BoardID: "block-id-1"} // blocks from workspace2. They're identical to blocks 1, 2 and 3, // and they should change - block4 := model.Block{ID: "block-id-1", RootID: "root-id-1"} - block5 := model.Block{ID: "block-id-2", RootID: "root-id-2", ParentID: "block-id-1"} - block6 := model.Block{ID: "block-id-6", RootID: "block-id-1", ParentID: "block-id-2"} + block4 := model.Block{ID: "block-id-1", BoardID: "board-id-1"} + block5 := model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} + block6 := model.Block{ID: "block-id-6", BoardID: "block-id-1", ParentID: "block-id-2"} // block from workspace3. It should change as well - block7 := model.Block{ID: "block-id-2", RootID: "root-id-2"} + block7 := model.Block{ID: "block-id-2", BoardID: "board-id-2"} for _, block := range []model.Block{block1, block2, block3} { - err := sqlStore.InsertBlock(container1, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container1, &block, "user-id-2") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } for _, block := range []model.Block{block4, block5, block6} { - err := sqlStore.InsertBlock(container2, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container2, &block, "user-id-2") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } for _, block := range []model.Block{block7} { - err := sqlStore.InsertBlock(container3, &block, "user-id") + err := sqlStore.insertLegacyBlock(sqlStore.db, container3, &block, "user-id-2") require.NoError(t, err) time.Sleep(100 * time.Millisecond) } @@ -193,39 +198,39 @@ func TestRunUniqueIDsMigration(t *testing.T) { require.NoError(t, err) // blocks from workspace 1 haven't changed, so we can simply fetch them - newBlock1, err := sqlStore.GetBlock(container1, block1.ID) + newBlock1, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block1.ID) require.NoError(t, err) require.NotNil(t, newBlock1) - newBlock2, err := sqlStore.GetBlock(container1, block2.ID) + newBlock2, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block2.ID) require.NoError(t, err) require.NotNil(t, newBlock2) - newBlock3, err := sqlStore.GetBlock(container1, block3.ID) + newBlock3, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block3.ID) require.NoError(t, err) require.NotNil(t, newBlock3) // first two blocks from workspace 2 have changed, so we fetch // them through the third one, which points to the new IDs - newBlock6, err := sqlStore.GetBlock(container2, block6.ID) + newBlock6, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block6.ID) require.NoError(t, err) require.NotNil(t, newBlock6) - newBlock4, err := sqlStore.GetBlock(container2, newBlock6.RootID) + newBlock4, err := sqlStore.getLegacyBlock(sqlStore.db, container2, newBlock6.BoardID) require.NoError(t, err) require.NotNil(t, newBlock4) - newBlock5, err := sqlStore.GetBlock(container2, newBlock6.ParentID) + newBlock5, err := sqlStore.getLegacyBlock(sqlStore.db, container2, newBlock6.ParentID) require.NoError(t, err) require.NotNil(t, newBlock5) // block from workspace 3 changed as well, so we shouldn't be able // to fetch it - newBlock7, err := sqlStore.GetBlock(container3, block7.ID) + newBlock7, err := sqlStore.getLegacyBlock(sqlStore.db, container3, block7.ID) require.NoError(t, err) require.Nil(t, newBlock7) // workspace 1 block links are maintained require.Equal(t, newBlock1.ID, newBlock2.ParentID) - require.Equal(t, newBlock1.ID, newBlock3.RootID) + require.Equal(t, newBlock1.ID, newBlock3.BoardID) // workspace 2 first two block IDs have changed - require.NotEqual(t, block4.ID, newBlock4.RootID) + require.NotEqual(t, block4.ID, newBlock4.BoardID) require.NotEqual(t, block5.ID, newBlock5.ParentID) } diff --git a/server/services/store/sqlstore/helpers_test.go b/server/services/store/sqlstore/helpers_test.go index 630643ceb..3916be364 100644 --- a/server/services/store/sqlstore/helpers_test.go +++ b/server/services/store/sqlstore/helpers_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "database/sql" + "os" "testing" "github.com/mattermost/focalboard/server/services/store" @@ -36,6 +37,9 @@ func SetupTests(t *testing.T) (store.Store, func()) { defer func() { _ = logger.Shutdown() }() err = store.Shutdown() require.Nil(t, err) + if err = os.Remove(connectionString); err == nil { + logger.Debug("Removed test database", mlog.String("file", connectionString)) + } } return store, tearDown diff --git a/server/services/store/sqlstore/legacy_blocks.go b/server/services/store/sqlstore/legacy_blocks.go new file mode 100644 index 000000000..fc3bf6134 --- /dev/null +++ b/server/services/store/sqlstore/legacy_blocks.go @@ -0,0 +1,208 @@ +package sqlstore + +import ( + "database/sql" + "encoding/json" + + "github.com/mattermost/focalboard/server/utils" + + sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +// legacyBlocksFromRows is the old getBlock version that still uses +// the old block model. This method is kept to enable the unique IDs +// data migration. +//nolint:unused +func (s *SQLStore) legacyBlocksFromRows(rows *sql.Rows) ([]model.Block, error) { + results := []model.Block{} + + for rows.Next() { + var block model.Block + var fieldsJSON string + var modifiedBy sql.NullString + var insertAt string + + err := rows.Scan( + &block.ID, + &block.ParentID, + &block.BoardID, + &block.CreatedBy, + &modifiedBy, + &block.Schema, + &block.Type, + &block.Title, + &fieldsJSON, + &insertAt, + &block.CreateAt, + &block.UpdateAt, + &block.DeleteAt, + &block.WorkspaceID) + if err != nil { + // handle this error + s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err)) + + return nil, err + } + + if modifiedBy.Valid { + block.ModifiedBy = modifiedBy.String + } + + err = json.Unmarshal([]byte(fieldsJSON), &block.Fields) + if err != nil { + // handle this error + s.logger.Error(`ERROR blocksFromRows fields`, mlog.Err(err)) + + return nil, err + } + + results = append(results, block) + } + + return results, nil +} + +// getLegacyBlock is the old getBlock version that still uses the old +// block model. This method is kept to enable the unique IDs data +// migration. +//nolint:unused +func (s *SQLStore) getLegacyBlock(db sq.BaseRunner, workspaceID string, blockID string) (*model.Block, error) { + query := s.getQueryBuilder(db). + Select( + "id", + "parent_id", + "root_id", + "created_by", + "modified_by", + s.escapeField("schema"), + "type", + "title", + "COALESCE(fields, '{}')", + "insert_at", + "create_at", + "update_at", + "delete_at", + "COALESCE(workspace_id, '0')", + ). + From(s.tablePrefix + "blocks"). + Where(sq.Eq{"id": blockID}). + Where(sq.Eq{"coalesce(workspace_id, '0')": workspaceID}) + + rows, err := query.Query() + if err != nil { + s.logger.Error(`GetBlock ERROR`, mlog.Err(err)) + return nil, err + } + + blocks, err := s.legacyBlocksFromRows(rows) + if err != nil { + return nil, err + } + + if len(blocks) == 0 { + return nil, nil + } + + return &blocks[0], nil +} + +// insertLegacyBlock is the old insertBlock version that still uses +// the old block model. This method is kept to enable the unique IDs +// data migration. +//nolint:unused +func (s *SQLStore) insertLegacyBlock(db sq.BaseRunner, workspaceID string, block *model.Block, userID string) error { + if block.BoardID == "" { + return BoardIDNilError{} + } + + fieldsJSON, err := json.Marshal(block.Fields) + if err != nil { + return err + } + + existingBlock, err := s.getLegacyBlock(db, workspaceID, block.ID) + if err != nil { + return err + } + + block.UpdateAt = utils.GetMillis() + block.ModifiedBy = userID + + insertQuery := s.getQueryBuilder(db).Insert(""). + Columns( + "workspace_id", + "id", + "parent_id", + "root_id", + "created_by", + "modified_by", + s.escapeField("schema"), + "type", + "title", + "fields", + "create_at", + "update_at", + "delete_at", + ) + + insertQueryValues := map[string]interface{}{ + "workspace_id": workspaceID, + "id": block.ID, + "parent_id": block.ParentID, + "root_id": block.BoardID, + s.escapeField("schema"): block.Schema, + "type": block.Type, + "title": block.Title, + "fields": fieldsJSON, + "delete_at": block.DeleteAt, + "created_by": block.CreatedBy, + "modified_by": block.ModifiedBy, + "create_at": block.CreateAt, + "update_at": block.UpdateAt, + } + + if existingBlock != nil { + // block with ID exists, so this is an update operation + query := s.getQueryBuilder(db).Update(s.tablePrefix+"blocks"). + Where(sq.Eq{"id": block.ID}). + Where(sq.Eq{"COALESCE(workspace_id, '0')": workspaceID}). + Set("parent_id", block.ParentID). + Set("root_id", block.BoardID). + Set("modified_by", block.ModifiedBy). + Set(s.escapeField("schema"), block.Schema). + Set("type", block.Type). + Set("title", block.Title). + Set("fields", fieldsJSON). + Set("update_at", block.UpdateAt). + Set("delete_at", block.DeleteAt) + + if _, err := query.Exec(); err != nil { + s.logger.Error(`InsertBlock error occurred while updating existing block`, mlog.String("blockID", block.ID), mlog.Err(err)) + return err + } + } else { + block.CreatedBy = userID + block.CreateAt = utils.GetMillis() + + insertQueryValues["created_by"] = block.CreatedBy + insertQueryValues["create_at"] = block.CreateAt + insertQueryValues["update_at"] = block.UpdateAt + insertQueryValues["modified_by"] = block.ModifiedBy + + query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks") + if _, err := query.Exec(); err != nil { + return err + } + } + + // writing block history + query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks_history") + if _, err := query.Exec(); err != nil { + return err + } + + return nil +} diff --git a/server/services/store/sqlstore/migrate.go b/server/services/store/sqlstore/migrate.go index 0cbd2fad3..9adfa0cde 100644 --- a/server/services/store/sqlstore/migrate.go +++ b/server/services/store/sqlstore/migrate.go @@ -2,87 +2,42 @@ package sqlstore import ( "bytes" + "context" "database/sql" - "errors" + "embed" "fmt" - "io" - "io/ioutil" - "os" + "path/filepath" "text/template" + "github.com/mattermost/morph/models" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" + + "github.com/mattermost/morph" + drivers "github.com/mattermost/morph/drivers" + mysql "github.com/mattermost/morph/drivers/mysql" + postgres "github.com/mattermost/morph/drivers/postgres" + sqlite "github.com/mattermost/morph/drivers/sqlite" + mbindata "github.com/mattermost/morph/sources/go_bindata" + mysqldriver "github.com/go-sql-driver/mysql" - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/database" - "github.com/golang-migrate/migrate/v4/database/mysql" - "github.com/golang-migrate/migrate/v4/database/postgres" - "github.com/golang-migrate/migrate/v4/database/sqlite3" - "github.com/golang-migrate/migrate/v4/source" - _ "github.com/golang-migrate/migrate/v4/source/file" // fileystem driver - bindata "github.com/golang-migrate/migrate/v4/source/go_bindata" _ "github.com/lib/pq" // postgres driver - "github.com/mattermost/focalboard/server/services/store/sqlstore/migrations" + sq "github.com/Masterminds/squirrel" + + "github.com/mattermost/focalboard/server/model" "github.com/mattermost/mattermost-plugin-api/cluster" ) +//go:embed migrations +var assets embed.FS + const ( uniqueIDsMigrationRequiredVersion = 14 + + tempSchemaMigrationTableName = "temp_schema_migration" ) -type PrefixedMigration struct { - *bindata.Bindata - prefix string - postgres bool - sqlite bool - mysql bool - plugin bool -} - -func init() { - source.Register("prefixed-migrations", &PrefixedMigration{}) -} - -func (pm *PrefixedMigration) executeTemplate(r io.ReadCloser, identifier string) (io.ReadCloser, string, error) { - data, err := ioutil.ReadAll(r) - if err != nil { - return nil, "", err - } - tmpl, err := template.New("sql").Parse(string(data)) - if err != nil { - return nil, "", err - } - buffer := bytes.NewBufferString("") - params := map[string]interface{}{ - "prefix": pm.prefix, - "postgres": pm.postgres, - "sqlite": pm.sqlite, - "mysql": pm.mysql, - "plugin": pm.plugin, - } - err = tmpl.Execute(buffer, params) - if err != nil { - return nil, "", err - } - - return ioutil.NopCloser(bytes.NewReader(buffer.Bytes())), identifier, nil -} - -func (pm *PrefixedMigration) ReadUp(version uint) (io.ReadCloser, string, error) { - r, identifier, err := pm.Bindata.ReadUp(version) - if err != nil { - return nil, "", err - } - return pm.executeTemplate(r, identifier) -} - -func (pm *PrefixedMigration) ReadDown(version uint) (io.ReadCloser, string, error) { - r, identifier, err := pm.Bindata.ReadDown(version) - if err != nil { - return nil, "", err - } - return pm.executeTemplate(r, identifier) -} - func appendMultipleStatementsFlag(connectionString string) (string, error) { config, err := mysqldriver.ParseDSN(connectionString) if err != nil { @@ -102,7 +57,7 @@ func appendMultipleStatementsFlag(connectionString string) (string, error) { // enabled. func (s *SQLStore) getMigrationConnection() (*sql.DB, error) { connectionString := s.connectionString - if s.dbType == mysqlDBType { + if s.dbType == model.MysqlDBType { var err error connectionString, err = appendMultipleStatementsFlag(s.connectionString) if err != nil { @@ -123,57 +78,104 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) { } func (s *SQLStore) Migrate() error { - var driver database.Driver + var driver drivers.Driver var err error - migrationsTable := fmt.Sprintf("%sschema_migrations", s.tablePrefix) - if s.dbType == sqliteDBType { - driver, err = sqlite3.WithInstance(s.db, &sqlite3.Config{MigrationsTable: migrationsTable}) + migrationConfig := drivers.Config{ + StatementTimeoutInSecs: 1000000, + MigrationsTable: fmt.Sprintf("%sschema_migrations", s.tablePrefix), + } + + if s.dbType == model.SqliteDBType { + driver, err = sqlite.WithInstance(s.db, &sqlite.Config{Config: migrationConfig}) if err != nil { return err } } - db, err := s.getMigrationConnection() - if err != nil { - return err - } - defer db.Close() + var db *sql.DB + if s.dbType != model.SqliteDBType { + db, err = s.getMigrationConnection() + if err != nil { + return err + } - if s.dbType == postgresDBType { - driver, err = postgres.WithInstance(db, &postgres.Config{MigrationsTable: migrationsTable}) + defer db.Close() + } + + if s.dbType == model.PostgresDBType { + driver, err = postgres.WithInstance(db, &postgres.Config{Config: migrationConfig}) if err != nil { return err } } - if s.dbType == mysqlDBType { - driver, err = mysql.WithInstance(db, &mysql.Config{MigrationsTable: migrationsTable}) + if s.dbType == model.MysqlDBType { + driver, err = mysql.WithInstance(db, &mysql.Config{Config: migrationConfig}) if err != nil { return err } } - bresource := bindata.Resource(migrations.AssetNames(), migrations.Asset) - - d, err := bindata.WithInstance(bresource) + assetsList, err := assets.ReadDir("migrations") if err != nil { return err } - - prefixedData := &PrefixedMigration{ - Bindata: d.(*bindata.Bindata), - prefix: s.tablePrefix, - plugin: s.isPlugin, - postgres: s.dbType == postgresDBType, - sqlite: s.dbType == sqliteDBType, - mysql: s.dbType == mysqlDBType, + assetNamesForDriver := make([]string, len(assetsList)) + for i, dirEntry := range assetsList { + assetNamesForDriver[i] = dirEntry.Name() } - m, err := migrate.NewWithInstance("prefixed-migration", prefixedData, s.dbType, driver) + params := map[string]interface{}{ + "prefix": s.tablePrefix, + "postgres": s.dbType == model.PostgresDBType, + "sqlite": s.dbType == model.SqliteDBType, + "mysql": s.dbType == model.MysqlDBType, + "plugin": s.isPlugin, + } + + migrationAssets := &mbindata.AssetSource{ + Names: assetNamesForDriver, + AssetFunc: func(name string) ([]byte, error) { + asset, mErr := assets.ReadFile(filepath.Join("migrations", name)) + if mErr != nil { + return nil, mErr + } + + tmpl, pErr := template.New("sql").Parse(string(asset)) + if pErr != nil { + return nil, pErr + } + buffer := bytes.NewBufferString("") + + err = tmpl.Execute(buffer, params) + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil + }, + } + + src, err := mbindata.WithInstance(migrationAssets) if err != nil { return err } + defer src.Close() + + opts := []morph.EngineOption{ + morph.WithLock("mm-lock-key"), + } + + if s.dbType == model.SqliteDBType { + opts = opts[:0] // sqlite driver does not support locking, it doesn't need to anyway. + } + + engine, err := morph.New(context.Background(), driver, src, opts...) + if err != nil { + return err + } + defer engine.Close() var mutex *cluster.Mutex if s.isPlugin { @@ -184,15 +186,19 @@ func (s *SQLStore) Migrate() error { } } - if err := ensureMigrationsAppliedUpToVersion(m, uniqueIDsMigrationRequiredVersion); err != nil { - return err - } - if s.isPlugin { s.logger.Debug("Acquiring cluster lock for Unique IDs migration") mutex.Lock() } + if err := s.migrateSchemaVersionTable(src.Migrations()); err != nil { + return err + } + + if err := ensureMigrationsAppliedUpToVersion(engine, driver, uniqueIDsMigrationRequiredVersion); err != nil { + return err + } + if err := s.runUniqueIDsMigration(); err != nil { if s.isPlugin { s.logger.Debug("Releasing cluster lock for Unique IDs migration") @@ -201,24 +207,248 @@ func (s *SQLStore) Migrate() error { return fmt.Errorf("error running unique IDs migration: %w", err) } + if err := ensureMigrationsAppliedUpToVersion(engine, driver, categoriesUUIDIDMigrationRequiredVersion); err != nil { + return err + } + + if err := s.runCategoryUUIDIDMigration(); err != nil { + if s.isPlugin { + s.logger.Debug("Releasing cluster lock for Unique IDs migration") + mutex.Unlock() + } + return fmt.Errorf("error running categoryID migration: %w", err) + } + + if err := s.deleteOldSchemaMigrationTable(); err != nil { + return err + } + if s.isPlugin { s.logger.Debug("Releasing cluster lock for Unique IDs migration") mutex.Unlock() } - if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) && !errors.Is(err, os.ErrNotExist) { + return engine.ApplyAll() +} + +// migrateSchemaVersionTable converts the schema version table from +// the old format used by go-migrate to the new format used by +// gomorph. +// When running the Focalboard with go-migrate's schema version table +// existing in the database, gomorph is unable to make sense of it as it's +// not in the format required by gomorph. +func (s *SQLStore) migrateSchemaVersionTable(migrations []*models.Migration) error { + migrationNeeded, err := s.isSchemaMigrationNeeded() + if err != nil { + return err + } + + if !migrationNeeded { + return nil + } + + s.logger.Info("Migrating schema migration to new format") + + legacySchemaVersion, err := s.getLegacySchemaVersion() + if err != nil { + return err + } + + if err := s.createTempSchemaTable(); err != nil { + return err + } + + if err := s.populateTempSchemaTable(migrations, legacySchemaVersion); err != nil { + return err + } + + if err := s.useNewSchemaTable(); err != nil { return err } return nil } -func ensureMigrationsAppliedUpToVersion(m *migrate.Migrate, version uint) error { - currentVersion, _, err := m.Version() - if err != nil && !errors.Is(err, migrate.ErrNilVersion) { +func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) { + // Check if `dirty` column exists on schema version table. + // This column exists only for the old schema version table. + + // SQLite needs a bit of a special handling + if s.dbType == model.SqliteDBType { + return s.isSchemaMigrationNeededSQLite() + } + + query := s.getQueryBuilder(s.db). + Select("count(*)"). + From("information_schema.COLUMNS"). + Where(sq.Eq{ + "TABLE_NAME": s.tablePrefix + "schema_migrations", + "COLUMN_NAME": "dirty", + }) + + row := query.QueryRow() + + var count int + if err := row.Scan(&count); err != nil { + s.logger.Error("failed to check for columns of schema_migrations table", mlog.Err(err)) + return false, err + } + + return count == 1, nil +} + +func (s *SQLStore) isSchemaMigrationNeededSQLite() (bool, error) { + // the way to check presence of a column is different + // for SQLite. Hence, the separate function + + query := fmt.Sprintf("PRAGMA table_info(\"%sschema_migrations\");", s.tablePrefix) + rows, err := s.db.Query(query) + if err != nil { + s.logger.Error("SQLite - failed to check for columns in schema_migrations table", mlog.Err(err)) + return false, err + } + + defer s.CloseRows(rows) + + data := [][]*string{} + for rows.Next() { + // PRAGMA returns 6 columns + row := make([]*string, 6) + + err := rows.Scan( + &row[0], + &row[1], + &row[2], + &row[3], + &row[4], + &row[5], + ) + if err != nil { + s.logger.Error("error scanning rows from SQLite schema_migrations table definition", mlog.Err(err)) + return false, err + } + + data = append(data, row) + } + + nameColumnFound := false + for _, row := range data { + if len(row) >= 2 && *row[1] == "dirty" { + nameColumnFound = true + break + } + } + + return nameColumnFound, nil +} + +func (s *SQLStore) getLegacySchemaVersion() (uint32, error) { + query := s.getQueryBuilder(s.db). + Select("version"). + From(s.tablePrefix + "schema_migrations") + + row := query.QueryRow() + + var version uint32 + if err := row.Scan(&version); err != nil { + s.logger.Error("error fetching legacy schema version", mlog.Err(err)) + s.logger.Error("getLegacySchemaVersion err " + err.Error()) + return version, err + } + + return version, nil +} + +func (s *SQLStore) createTempSchemaTable() error { + // squirrel doesn't support DDL query in query builder + // so, we need to use a plain old string + query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (Version bigint NOT NULL, Name varchar(64) NOT NULL, PRIMARY KEY (Version))", s.tablePrefix+tempSchemaMigrationTableName) + if _, err := s.db.Exec(query); err != nil { + s.logger.Error("failed to create temporary schema migration table", mlog.Err(err)) + s.logger.Error("createTempSchemaTable error " + err.Error()) return err } + return nil +} +func (s *SQLStore) populateTempSchemaTable(migrations []*models.Migration, legacySchemaVersion uint32) error { + query := s.getQueryBuilder(s.db). + Insert(s.tablePrefix+tempSchemaMigrationTableName). + Columns("Version", "Name") + + for _, migration := range migrations { + // migrations param contains both up and down variant for + // each migration. Skipping for either one (down in this case) + // to process a migration only a single time. + if migration.Direction == models.Down { + continue + } + + if migration.Version > legacySchemaVersion { + break + } + + query = query.Values(migration.Version, migration.Name) + } + + if _, err := query.Exec(); err != nil { + s.logger.Error("failed to insert migration records into temporary schema table", mlog.Err(err)) + return err + } + + return nil +} + +func (s *SQLStore) useNewSchemaTable() error { + // first delete the old table, then + // rename the new table to old table's name + + // renaming old schema migration table. Will delete later once the migration is + // complete, just in case. + var query string + if s.dbType == model.MysqlDBType { + query = fmt.Sprintf("RENAME TABLE `%sschema_migrations` TO `%sschema_migrations_old_temp`", s.tablePrefix, s.tablePrefix) + } else { + query = fmt.Sprintf("ALTER TABLE %sschema_migrations RENAME TO %sschema_migrations_old_temp", s.tablePrefix, s.tablePrefix) + } + + if _, err := s.db.Exec(query); err != nil { + s.logger.Error("failed to rename old schema migration table", mlog.Err(err)) + return err + } + + // renaming new temp table to old table's name + if s.dbType == model.MysqlDBType { + query = fmt.Sprintf("RENAME TABLE `%s%s` TO `%sschema_migrations`", s.tablePrefix, tempSchemaMigrationTableName, s.tablePrefix) + } else { + query = fmt.Sprintf("ALTER TABLE %s%s RENAME TO %sschema_migrations", s.tablePrefix, tempSchemaMigrationTableName, s.tablePrefix) + } + + if _, err := s.db.Exec(query); err != nil { + s.logger.Error("failed to rename temp schema table", mlog.Err(err)) + return err + } + + return nil +} + +func (s *SQLStore) deleteOldSchemaMigrationTable() error { + query := "DROP TABLE IF EXISTS " + s.tablePrefix + "schema_migrations_old_temp" + if _, err := s.db.Exec(query); err != nil { + s.logger.Error("failed to delete old temp schema migrations table", mlog.Err(err)) + return err + } + + return nil +} + +func ensureMigrationsAppliedUpToVersion(engine *morph.Morph, driver drivers.Driver, version int) error { + applied, err := driver.AppliedMigrations() + if err != nil { + return err + } + currentVersion := len(applied) + // if the target version is below or equal to the current one, do // not migrate either because is not needed (both are equal) or // because it would downgrade the database (is below) @@ -226,7 +456,7 @@ func ensureMigrationsAppliedUpToVersion(m *migrate.Migrate, version uint) error return nil } - if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) && !errors.Is(err, os.ErrNotExist) { + if _, err = engine.Apply(version - currentVersion); err != nil { return err } diff --git a/server/services/store/sqlstore/migrations/migrations_files/000001_init.down.sql b/server/services/store/sqlstore/migrations/000001_init.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000001_init.down.sql rename to server/services/store/sqlstore/migrations/000001_init.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000001_init.up.sql b/server/services/store/sqlstore/migrations/000001_init.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000001_init.up.sql rename to server/services/store/sqlstore/migrations/000001_init.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000002_system_settings_table.down.sql b/server/services/store/sqlstore/migrations/000002_system_settings_table.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000002_system_settings_table.down.sql rename to server/services/store/sqlstore/migrations/000002_system_settings_table.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000002_system_settings_table.up.sql b/server/services/store/sqlstore/migrations/000002_system_settings_table.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000002_system_settings_table.up.sql rename to server/services/store/sqlstore/migrations/000002_system_settings_table.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000003_blocks_rootid.down.sql b/server/services/store/sqlstore/migrations/000003_blocks_rootid.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000003_blocks_rootid.down.sql rename to server/services/store/sqlstore/migrations/000003_blocks_rootid.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000003_blocks_rootid.up.sql b/server/services/store/sqlstore/migrations/000003_blocks_rootid.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000003_blocks_rootid.up.sql rename to server/services/store/sqlstore/migrations/000003_blocks_rootid.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000004_auth_table.down.sql b/server/services/store/sqlstore/migrations/000004_auth_table.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000004_auth_table.down.sql rename to server/services/store/sqlstore/migrations/000004_auth_table.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000004_auth_table.up.sql b/server/services/store/sqlstore/migrations/000004_auth_table.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000004_auth_table.up.sql rename to server/services/store/sqlstore/migrations/000004_auth_table.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000005_blocks_modifiedby.down.sql b/server/services/store/sqlstore/migrations/000005_blocks_modifiedby.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000005_blocks_modifiedby.down.sql rename to server/services/store/sqlstore/migrations/000005_blocks_modifiedby.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000005_blocks_modifiedby.up.sql b/server/services/store/sqlstore/migrations/000005_blocks_modifiedby.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000005_blocks_modifiedby.up.sql rename to server/services/store/sqlstore/migrations/000005_blocks_modifiedby.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000006_sharing_table.down.sql b/server/services/store/sqlstore/migrations/000006_sharing_table.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000006_sharing_table.down.sql rename to server/services/store/sqlstore/migrations/000006_sharing_table.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000006_sharing_table.up.sql b/server/services/store/sqlstore/migrations/000006_sharing_table.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000006_sharing_table.up.sql rename to server/services/store/sqlstore/migrations/000006_sharing_table.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000007_workspaces_table.down.sql b/server/services/store/sqlstore/migrations/000007_workspaces_table.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000007_workspaces_table.down.sql rename to server/services/store/sqlstore/migrations/000007_workspaces_table.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000007_workspaces_table.up.sql b/server/services/store/sqlstore/migrations/000007_workspaces_table.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000007_workspaces_table.up.sql rename to server/services/store/sqlstore/migrations/000007_workspaces_table.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000008_teams.down.sql b/server/services/store/sqlstore/migrations/000008_teams.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000008_teams.down.sql rename to server/services/store/sqlstore/migrations/000008_teams.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000008_teams.up.sql b/server/services/store/sqlstore/migrations/000008_teams.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000008_teams.up.sql rename to server/services/store/sqlstore/migrations/000008_teams.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000009_blocks_history.down.sql b/server/services/store/sqlstore/migrations/000009_blocks_history.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000009_blocks_history.down.sql rename to server/services/store/sqlstore/migrations/000009_blocks_history.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000009_blocks_history.up.sql b/server/services/store/sqlstore/migrations/000009_blocks_history.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000009_blocks_history.up.sql rename to server/services/store/sqlstore/migrations/000009_blocks_history.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000010_blocks_created_by.down.sql b/server/services/store/sqlstore/migrations/000010_blocks_created_by.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000010_blocks_created_by.down.sql rename to server/services/store/sqlstore/migrations/000010_blocks_created_by.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000010_blocks_created_by.up.sql b/server/services/store/sqlstore/migrations/000010_blocks_created_by.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000010_blocks_created_by.up.sql rename to server/services/store/sqlstore/migrations/000010_blocks_created_by.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000011_match_collation.down.sql b/server/services/store/sqlstore/migrations/000011_match_collation.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000011_match_collation.down.sql rename to server/services/store/sqlstore/migrations/000011_match_collation.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000011_match_collation.up.sql b/server/services/store/sqlstore/migrations/000011_match_collation.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000011_match_collation.up.sql rename to server/services/store/sqlstore/migrations/000011_match_collation.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000012_match_column_collation.down.sql b/server/services/store/sqlstore/migrations/000012_match_column_collation.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000012_match_column_collation.down.sql rename to server/services/store/sqlstore/migrations/000012_match_column_collation.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000012_match_column_collation.up.sql b/server/services/store/sqlstore/migrations/000012_match_column_collation.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000012_match_column_collation.up.sql rename to server/services/store/sqlstore/migrations/000012_match_column_collation.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000013_millisecond_timestamps.down.sql b/server/services/store/sqlstore/migrations/000013_millisecond_timestamps.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000013_millisecond_timestamps.down.sql rename to server/services/store/sqlstore/migrations/000013_millisecond_timestamps.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000013_millisecond_timestamps.up.sql b/server/services/store/sqlstore/migrations/000013_millisecond_timestamps.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000013_millisecond_timestamps.up.sql rename to server/services/store/sqlstore/migrations/000013_millisecond_timestamps.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000014_add_not_null_constraint.down.sql b/server/services/store/sqlstore/migrations/000014_add_not_null_constraint.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000014_add_not_null_constraint.down.sql rename to server/services/store/sqlstore/migrations/000014_add_not_null_constraint.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000014_add_not_null_constraint.up.sql b/server/services/store/sqlstore/migrations/000014_add_not_null_constraint.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000014_add_not_null_constraint.up.sql rename to server/services/store/sqlstore/migrations/000014_add_not_null_constraint.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000015_blocks_history_no_nulls.down.sql b/server/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000015_blocks_history_no_nulls.down.sql rename to server/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000015_blocks_history_no_nulls.up.sql b/server/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000015_blocks_history_no_nulls.up.sql rename to server/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.up.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000016_subscriptions_table.down.sql b/server/services/store/sqlstore/migrations/000016_subscriptions_table.down.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000016_subscriptions_table.down.sql rename to server/services/store/sqlstore/migrations/000016_subscriptions_table.down.sql diff --git a/server/services/store/sqlstore/migrations/migrations_files/000016_subscriptions_table.up.sql b/server/services/store/sqlstore/migrations/000016_subscriptions_table.up.sql similarity index 100% rename from server/services/store/sqlstore/migrations/migrations_files/000016_subscriptions_table.up.sql rename to server/services/store/sqlstore/migrations/000016_subscriptions_table.up.sql diff --git a/server/services/store/sqlstore/migrations/000017_add_teams_and_boards.down.sql b/server/services/store/sqlstore/migrations/000017_add_teams_and_boards.down.sql new file mode 100644 index 000000000..e0ac49d1e --- /dev/null +++ b/server/services/store/sqlstore/migrations/000017_add_teams_and_boards.down.sql @@ -0,0 +1 @@ +SELECT 1; diff --git a/server/services/store/sqlstore/migrations/000017_add_teams_and_boards.up.sql b/server/services/store/sqlstore/migrations/000017_add_teams_and_boards.up.sql new file mode 100644 index 000000000..2ed63ba57 --- /dev/null +++ b/server/services/store/sqlstore/migrations/000017_add_teams_and_boards.up.sql @@ -0,0 +1,378 @@ +{{if .mysql}} +RENAME TABLE {{.prefix}}workspaces TO {{.prefix}}teams; +ALTER TABLE {{.prefix}}blocks CHANGE workspace_id channel_id VARCHAR(36); +ALTER TABLE {{.prefix}}blocks_history CHANGE workspace_id channel_id VARCHAR(36); +{{else}} +ALTER TABLE {{.prefix}}workspaces RENAME TO {{.prefix}}teams; +ALTER TABLE {{.prefix}}blocks RENAME COLUMN workspace_id TO channel_id; +ALTER TABLE {{.prefix}}blocks_history RENAME COLUMN workspace_id TO channel_id; +{{end}} +ALTER TABLE {{.prefix}}blocks ADD COLUMN board_id VARCHAR(36); +ALTER TABLE {{.prefix}}blocks_history ADD COLUMN board_id VARCHAR(36); + +{{- /* cleanup incorrect data format in column calculations */ -}} +{{if .mysql}} +UPDATE {{.prefix}}blocks SET fields = JSON_SET(fields, '$.columnCalculations', cast('{}' as json)) WHERE fields->'$.columnCalculations' = cast('[]' as json); +{{end}} + +{{if .postgres}} +UPDATE {{.prefix}}blocks SET fields = fields::jsonb - 'columnCalculations' || '{"columnCalculations": {}}' WHERE fields->>'columnCalculations' = '[]'; +{{end}} + +{{if .sqlite}} +UPDATE {{.prefix}}blocks SET fields = replace(fields, '"columnCalculations":[]', '"columnCalculations":{}'); +{{end}} + +{{- /* add boards tables */ -}} +CREATE TABLE {{.prefix}}boards ( + id VARCHAR(36) NOT NULL PRIMARY KEY, + + {{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}} + {{if .sqlite}}insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),{{end}} + {{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}} + + team_id VARCHAR(36) NOT NULL, + channel_id VARCHAR(36), + created_by VARCHAR(36), + modified_by VARCHAR(36), + type VARCHAR(1) NOT NULL, + title TEXT, + description TEXT, + icon VARCHAR(256), + show_description BOOLEAN, + is_template BOOLEAN, + template_version INT DEFAULT 0, + {{if .mysql}} + properties JSON, + card_properties JSON, + column_calculations JSON, + {{end}} + {{if .postgres}} + properties JSONB, + card_properties JSONB, + column_calculations JSONB, + {{end}} + {{if .sqlite}} + properties TEXT, + card_properties TEXT, + column_calculations TEXT, + {{end}} + create_at BIGINT, + update_at BIGINT, + delete_at BIGINT +) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; + +CREATE TABLE {{.prefix}}boards_history ( + id VARCHAR(36) NOT NULL, + + {{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}} + {{if .sqlite}}insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),{{end}} + {{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}} + + team_id VARCHAR(36) NOT NULL, + channel_id VARCHAR(36), + created_by VARCHAR(36), + modified_by VARCHAR(36), + type VARCHAR(1) NOT NULL, + title TEXT, + description TEXT, + icon VARCHAR(256), + show_description BOOLEAN, + is_template BOOLEAN, + template_version INT DEFAULT 0, + {{if .mysql}} + properties JSON, + card_properties JSON, + column_calculations JSON, + {{end}} + {{if .postgres}} + properties JSONB, + card_properties JSONB, + column_calculations JSONB, + {{end}} + {{if .sqlite}} + properties TEXT, + card_properties TEXT, + column_calculations TEXT, + {{end}} + create_at BIGINT, + update_at BIGINT, + delete_at BIGINT, + + PRIMARY KEY (id, insert_at) +) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; + + +{{- /* migrate board blocks to boards table */ -}} +{{if .plugin}} + {{if .postgres}} + INSERT INTO {{.prefix}}boards ( + SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type, B.title, (B.fields->>'description')::text, + B.fields->>'icon', (B.fields->'showDescription')::text::boolean, (B.fields->'isTemplate')::text::boolean, + COALESCE((B.fields->'templateVer')::text, '0')::int, + '{}', B.fields->'cardProperties', B.fields->'columnCalculations', B.create_at, + B.update_at, B.delete_at + FROM {{.prefix}}blocks AS B + INNER JOIN channels AS C ON C.Id=B.channel_id + WHERE B.type='board' + ); + INSERT INTO {{.prefix}}boards_history ( + SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type, B.title, (B.fields->>'description')::text, + B.fields->>'icon', (B.fields->'showDescription')::text::boolean, (B.fields->'isTemplate')::text::boolean, + COALESCE((B.fields->'templateVer')::text, '0')::int, + '{}', B.fields->'cardProperties', B.fields->'columnCalculations', B.create_at, + B.update_at, B.delete_at + FROM {{.prefix}}blocks_history AS B + INNER JOIN channels AS C ON C.Id=B.channel_id + WHERE B.type='board' + ); + {{end}} + {{if .mysql}} + INSERT INTO {{.prefix}}boards ( + SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.Type, B.title, JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), + JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.icon')), + COALESCE(B.fields->'$.showDescription', 'false') = 'true', + COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true', + COALESCE(B.fields->'$.templateVer', 0), + '{}', B.fields->'$.cardProperties', B.fields->'$.columnCalculations', B.create_at, + B.update_at, B.delete_at + FROM {{.prefix}}blocks AS B + INNER JOIN Channels AS C ON C.Id=B.channel_id + WHERE B.type='board' + ); + INSERT INTO {{.prefix}}boards_history ( + SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.Type, B.title, JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), + JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.icon')), + COALESCE(B.fields->'$.showDescription', 'false') = 'true', + COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true', + COALESCE(B.fields->'$.templateVer', 0), + '{}', B.fields->'$.cardProperties', B.fields->'$.columnCalculations', B.create_at, + B.update_at, B.delete_at + FROM {{.prefix}}blocks_history AS B + INNER JOIN Channels AS C ON C.Id=B.channel_id + WHERE B.type='board' + ); + {{end}} +{{else}} + {{if .postgres}} + INSERT INTO {{.prefix}}boards ( + SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, (fields->>'description')::text, + B.fields->>'icon', (fields->'showDescription')::text::boolean, (fields->'isTemplate')::text::boolean, + (B.fields->'templateVer')::text::int, + '{}', fields->'cardProperties', fields->'columnCalculations', create_at, + update_at, delete_at + FROM {{.prefix}}blocks AS B + WHERE type='board' + ); + INSERT INTO {{.prefix}}boards_history ( + SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, (fields->>'description')::text, + B.fields->>'icon', (fields->'showDescription')::text::boolean, (fields->'isTemplate')::text::boolean, + (B.fields->'templateVer')::text::int, + '{}', fields->'cardProperties', fields->'columnCalculations', create_at, + update_at, delete_at + FROM {{.prefix}}blocks_history AS B + WHERE type='board' + ); + {{end}} + {{if .mysql}} + INSERT INTO {{.prefix}}boards ( + SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, JSON_UNQUOTE(JSON_EXTRACT(fields,'$.description')), + JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')), fields->'$.showDescription', fields->'$.isTemplate', + B.fields->'$.templateVer', + '{}', fields->'$.cardProperties', fields->'$.columnCalculations', create_at, + update_at, delete_at + FROM {{.prefix}}blocks AS B + WHERE type='board' + ); + INSERT INTO {{.prefix}}boards_history ( + SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, JSON_UNQUOTE(JSON_EXTRACT(fields,'$.description')), + JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')), fields->'$.showDescription', fields->'$.isTemplate', + B.fields->'$.templateVer', + '{}', fields->'$.cardProperties', fields->'$.columnCalculations', create_at, + update_at, delete_at + FROM {{.prefix}}blocks_history AS B + WHERE type='board' + ); + {{end}} + {{if .sqlite}} + INSERT INTO {{.prefix}}boards + SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, json_extract(fields, '$.description'), + json_extract(fields, '$.icon'), json_extract(fields, '$.showDescription'), json_extract(fields, '$.isTemplate'), + json_extract(fields, '$.templateVer'), + '{}', json_extract(fields, '$.cardProperties'), json_extract(fields, '$.columnCalculations'), create_at, + update_at, delete_at + FROM {{.prefix}}blocks + WHERE type='board' + ; + INSERT INTO {{.prefix}}boards_history + SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, json_extract(fields, '$.description'), + json_extract(fields, '$.icon'), json_extract(fields, '$.showDescription'), json_extract(fields, '$.isTemplate'), + json_extract(fields, '$.templateVer'), + '{}', json_extract(fields, '$.cardProperties'), json_extract(fields, '$.columnCalculations'), create_at, + update_at, delete_at + FROM {{.prefix}}blocks_history + WHERE type='board' + ; + {{end}} +{{end}} + + +{{- /* Update block references to boards*/ -}} +{{if .sqlite}} + UPDATE {{.prefix}}blocks as B + SET board_id=(SELECT id FROM {{.prefix}}blocks WHERE id=B.parent_id AND type='board') + WHERE EXISTS (SELECT id FROM {{.prefix}}blocks WHERE id=B.parent_id AND type='board'); + + UPDATE {{.prefix}}blocks as B + SET board_id=(SELECT GP.id FROM {{.prefix}}blocks as GP JOIN {{.prefix}}blocks AS P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GP.type = 'board') + WHERE EXISTS (SELECT GP.id FROM {{.prefix}}blocks as GP JOIN {{.prefix}}blocks AS P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GP.type = 'board'); + + UPDATE {{.prefix}}blocks as B + SET board_id=(SELECT GGP.id FROM {{.prefix}}blocks as GGP JOIN {{.prefix}}blocks as GP ON GGP.id=GP.parent_id JOIN {{.prefix}}blocks as P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GGP.type = 'board') + WHERE EXISTS (SELECT GGP.id FROM {{.prefix}}blocks as GGP JOIN {{.prefix}}blocks as GP ON GGP.id=GP.parent_id JOIN {{.prefix}}blocks as P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GGP.type = 'board'); + + UPDATE {{.prefix}}blocks_history as B + SET board_id=(SELECT id FROM {{.prefix}}blocks_history WHERE id=B.parent_id AND type='board') + WHERE EXISTS (SELECT id FROM {{.prefix}}blocks_history WHERE id=B.parent_id AND type='board'); + + UPDATE {{.prefix}}blocks_history as B + SET board_id=(SELECT GP.id FROM {{.prefix}}blocks_history as GP JOIN {{.prefix}}blocks_history AS P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GP.type = 'board') + WHERE EXISTS (SELECT GP.id FROM {{.prefix}}blocks_history as GP JOIN {{.prefix}}blocks_history AS P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GP.type = 'board'); + + UPDATE {{.prefix}}blocks_history as B + SET board_id=(SELECT GGP.id FROM {{.prefix}}blocks_history as GGP JOIN {{.prefix}}blocks_history as GP ON GGP.id=GP.parent_id JOIN {{.prefix}}blocks_history as P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GGP.type = 'board') + WHERE EXISTS (SELECT GGP.id FROM {{.prefix}}blocks_history as GGP JOIN {{.prefix}}blocks_history as GP ON GGP.id=GP.parent_id JOIN {{.prefix}}blocks_history as P ON GP.id=P.parent_id WHERE P.id=B.parent_id AND GGP.type = 'board'); +{{end}} +{{if .mysql}} + UPDATE {{.prefix}}blocks as B +INNER JOIN {{.prefix}}blocks as P + ON B.parent_id=P.id + SET B.board_id=P.id + WHERE P.type = 'board'; + + UPDATE {{.prefix}}blocks as B +INNER JOIN {{.prefix}}blocks as P + ON B.parent_id=P.id +INNER JOIN {{.prefix}}blocks as GP + ON P.parent_id=GP.id + SET B.board_id=GP.id + WHERE GP.type = 'board'; + + UPDATE {{.prefix}}blocks as B +INNER JOIN {{.prefix}}blocks as P + ON B.parent_id=P.id +INNER JOIN {{.prefix}}blocks as GP + ON P.parent_id=GP.id +INNER JOIN {{.prefix}}blocks as GPP + ON GP.parent_id=GPP.id + SET B.board_id=GPP.id + WHERE GPP.type = 'board'; + + UPDATE {{.prefix}}blocks_history as B +INNER JOIN {{.prefix}}blocks_history as P + ON B.parent_id=P.id + SET B.board_id=P.id + WHERE P.type = 'board'; + + UPDATE {{.prefix}}blocks_history as B +INNER JOIN {{.prefix}}blocks_history as P + ON B.parent_id=P.id +INNER JOIN {{.prefix}}blocks_history as GP + ON P.parent_id=GP.id + SET B.board_id=GP.id + WHERE GP.type = 'board'; + + UPDATE {{.prefix}}blocks_history as B +INNER JOIN {{.prefix}}blocks_history as P + ON B.parent_id=P.id +INNER JOIN {{.prefix}}blocks_history as GP + ON P.parent_id=GP.id +INNER JOIN {{.prefix}}blocks_history as GPP + ON GP.parent_id=GPP.id + SET B.board_id=GPP.id + WHERE GPP.type = 'board'; +{{end}} +{{if .postgres}} + UPDATE {{.prefix}}blocks as B + SET board_id=P.id + FROM {{.prefix}}blocks as P + WHERE B.parent_id=P.id + AND P.type = 'board'; + + UPDATE {{.prefix}}blocks as B + SET board_id=GP.id + FROM {{.prefix}}blocks as P, + {{.prefix}}blocks as GP + WHERE B.parent_id=P.id + AND P.parent_id=GP.id + AND GP.type = 'board'; + + UPDATE {{.prefix}}blocks as B + SET board_id=GGP.id + FROM {{.prefix}}blocks as P, + {{.prefix}}blocks as GP, + {{.prefix}}blocks as GGP + WHERE B.parent_id=P.id + AND P.parent_id=GP.id + AND GP.parent_id=GGP.id + AND GGP.type = 'board'; + + UPDATE {{.prefix}}blocks_history as B + SET board_id=P.id + FROM {{.prefix}}blocks_history as P + WHERE B.parent_id=P.id + AND P.type = 'board'; + + UPDATE {{.prefix}}blocks_history as B + SET board_id=GP.id + FROM {{.prefix}}blocks_history as P, + {{.prefix}}blocks_history as GP + WHERE B.parent_id=P.id + AND P.parent_id=GP.id + AND GP.type = 'board'; + + UPDATE {{.prefix}}blocks_history as B + SET board_id=GGP.id + FROM {{.prefix}}blocks_history as P, + {{.prefix}}blocks_history as GP, + {{.prefix}}blocks_history as GGP + WHERE B.parent_id=P.id + AND P.parent_id=GP.id + AND GP.parent_id=GGP.id + AND GGP.type = 'board'; +{{end}} + + +{{- /* Remove boards, including templates */ -}} +DELETE FROM {{.prefix}}blocks WHERE type = 'board'; +DELETE FROM {{.prefix}}blocks_history WHERE type = 'board'; + +{{- /* add board_members */ -}} +CREATE TABLE {{.prefix}}board_members ( + board_id VARCHAR(36) NOT NULL, + user_id VARCHAR(36) NOT NULL, + roles VARCHAR(64), + scheme_admin BOOLEAN, + scheme_editor BOOLEAN, + scheme_commenter BOOLEAN, + scheme_viewer BOOLEAN, + PRIMARY KEY (board_id, user_id) +) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; + +CREATE INDEX idx_boardmembers_user_id ON {{.prefix}}board_members(user_id); + +{{if .plugin}} +{{- /* if we're in plugin, migrate channel memberships to the board */ -}} +INSERT INTO {{.prefix}}board_members ( + SELECT B.Id, CM.UserId, CM.Roles, (CM.UserId=B.created_by) OR CM.SchemeAdmin, CM.SchemeUser, FALSE, CM.SchemeGuest + FROM {{.prefix}}boards AS B + INNER JOIN ChannelMembers as CM ON CM.ChannelId=B.channel_id + WHERE NOT B.is_template +); +{{else}} +{{- /* if we're in personal server or desktop, create memberships for everyone */ -}} +INSERT INTO {{.prefix}}board_members + SELECT B.id, U.id, '', B.created_by=U.id, TRUE, FALSE, FALSE + FROM {{.prefix}}boards AS B, {{.prefix}}users AS U + WHERE NOT B.is_template; +{{end}} diff --git a/server/services/store/sqlstore/migrations/000018_populate_categories.down.sql b/server/services/store/sqlstore/migrations/000018_populate_categories.down.sql new file mode 100644 index 000000000..e01b744a7 --- /dev/null +++ b/server/services/store/sqlstore/migrations/000018_populate_categories.down.sql @@ -0,0 +1 @@ +DELETE from {{.prefix}}categories; diff --git a/server/services/store/sqlstore/migrations/000018_populate_categories.up.sql b/server/services/store/sqlstore/migrations/000018_populate_categories.up.sql new file mode 100644 index 000000000..fe6437694 --- /dev/null +++ b/server/services/store/sqlstore/migrations/000018_populate_categories.up.sql @@ -0,0 +1,51 @@ +CREATE TABLE {{.prefix}}categories ( + {{if .mysql}}id INT NOT NULL UNIQUE AUTO_INCREMENT,{{end}} + {{if .postgres}}id SERIAL,{{end}} + {{if .sqlite}}id varchar(36),{{end}} + name varchar(100) NOT NULL, + user_id varchar(32) NOT NULL, + team_id varchar(32) NOT NULL, + {{if not .sqlite}} + channel_id varchar(32) NOT NULL, + {{end}} + create_at BIGINT, + update_at BIGINT, + delete_at BIGINT, + PRIMARY KEY (id) + ) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; + +{{if .plugin}} + INSERT INTO {{.prefix}}categories( + name, + user_id, + team_id, + {{if not .sqlite}}channel_id,{{end}} + create_at, + update_at, + delete_at + ) + SELECT + COALESCE(nullif(c.DisplayName, ''), 'Direct Message') as category_name, + cm.UserId, + COALESCE(nullif(c.TeamId, ''), 'direct_message') as team_id, + {{if not .sqlite}}cm.ChannelId,{{end}} + {{if .postgres}}(extract(epoch from now())*1000)::bigint,{{end}} + {{if .mysql}}UNIX_TIMESTAMP() * 1000,{{end}} + {{if .sqlite}}CAST(strftime('%s', 'now') * 1000 as bigint),{{end}} + 0, + 0 + FROM + {{.prefix}}boards boards + JOIN ChannelMembers cm on boards.channel_id = cm.ChannelId + JOIN Channels c on cm.ChannelId = c.id + GROUP BY cm.UserId, c.TeamId, cm.ChannelId, c.DisplayName; + + {{if .mysql}} + ALTER TABLE {{.prefix}}categories MODIFY id varchar(36); + {{end}} + + {{if .postgres}} + ALTER TABLE {{.prefix}}categories ALTER COLUMN id TYPE varchar(36); + ALTER TABLE {{.prefix}}categories ALTER COLUMN id DROP DEFAULT; + {{end}} +{{end}} diff --git a/server/services/store/sqlstore/migrations/000019_populate_category_blocks.down.sql b/server/services/store/sqlstore/migrations/000019_populate_category_blocks.down.sql new file mode 100644 index 000000000..8619b631c --- /dev/null +++ b/server/services/store/sqlstore/migrations/000019_populate_category_blocks.down.sql @@ -0,0 +1 @@ +DELETE from {{.prefix}}category_blocks; diff --git a/server/services/store/sqlstore/migrations/000019_populate_category_blocks.up.sql b/server/services/store/sqlstore/migrations/000019_populate_category_blocks.up.sql new file mode 100644 index 000000000..51b7c495b --- /dev/null +++ b/server/services/store/sqlstore/migrations/000019_populate_category_blocks.up.sql @@ -0,0 +1,41 @@ +CREATE TABLE {{.prefix}}category_blocks ( + {{if .mysql}}id INT AUTO_INCREMENT,{{end}} + {{if .postgres}}id SERIAL,{{end}} + {{if .sqlite}}id varchar(36),{{end}} + user_id varchar(32) NOT NULL, + category_id varchar(36) NOT NULL, + block_id VARCHAR(36) NOT NULL, + create_at BIGINT, + update_at BIGINT, + delete_at BIGINT, + PRIMARY KEY (id) + ) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; + +{{if .plugin}} + INSERT INTO {{.prefix}}category_blocks(user_id, category_id, block_id, create_at, update_at, delete_at) + SELECT + {{.prefix}}categories.user_id, + {{.prefix}}categories.id, + {{.prefix}}boards.id, + {{if .postgres}}(extract(epoch from now())*1000)::bigint,{{end}} + {{if .mysql}}UNIX_TIMESTAMP() * 1000,{{end}} + {{if .sqlite}}CAST(strftime('%s', 'now') * 1000 as bigint),{{end}} + 0, + 0 + FROM + {{.prefix}}categories + JOIN {{.prefix}}boards ON {{.prefix}}categories.channel_id = {{.prefix}}boards.channel_id + AND {{.prefix}}boards.is_template = false +; + + ALTER TABLE {{.prefix}}categories DROP COLUMN channel_id; + + {{if .mysql}} + ALTER TABLE {{.prefix}}category_blocks MODIFY id varchar(36); + {{end}} + + {{if .postgres}} + ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id TYPE varchar(36); + ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id DROP DEFAULT; + {{end}} +{{end}} diff --git a/server/services/store/sqlstore/migrations/bindata.go b/server/services/store/sqlstore/migrations/bindata.go deleted file mode 100644 index 4e954aab0..000000000 --- a/server/services/store/sqlstore/migrations/bindata.go +++ /dev/null @@ -1,948 +0,0 @@ -// Code generated by go-bindata. -// sources: -// migrations_files/000001_init.down.sql -// migrations_files/000001_init.up.sql -// migrations_files/000002_system_settings_table.down.sql -// migrations_files/000002_system_settings_table.up.sql -// migrations_files/000003_blocks_rootid.down.sql -// migrations_files/000003_blocks_rootid.up.sql -// migrations_files/000004_auth_table.down.sql -// migrations_files/000004_auth_table.up.sql -// migrations_files/000005_blocks_modifiedby.down.sql -// migrations_files/000005_blocks_modifiedby.up.sql -// migrations_files/000006_sharing_table.down.sql -// migrations_files/000006_sharing_table.up.sql -// migrations_files/000007_workspaces_table.down.sql -// migrations_files/000007_workspaces_table.up.sql -// migrations_files/000008_teams.down.sql -// migrations_files/000008_teams.up.sql -// migrations_files/000009_blocks_history.down.sql -// migrations_files/000009_blocks_history.up.sql -// migrations_files/000010_blocks_created_by.down.sql -// migrations_files/000010_blocks_created_by.up.sql -// migrations_files/000011_match_collation.down.sql -// migrations_files/000011_match_collation.up.sql -// migrations_files/000012_match_column_collation.down.sql -// migrations_files/000012_match_column_collation.up.sql -// migrations_files/000013_millisecond_timestamps.down.sql -// migrations_files/000013_millisecond_timestamps.up.sql -// migrations_files/000014_add_not_null_constraint.down.sql -// migrations_files/000014_add_not_null_constraint.up.sql -// migrations_files/000015_blocks_history_no_nulls.down.sql -// migrations_files/000015_blocks_history_no_nulls.up.sql -// migrations_files/000016_subscriptions_table.down.sql -// migrations_files/000016_subscriptions_table.up.sql -// DO NOT EDIT! - -package migrations - -import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) - -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -type asset struct { - bytes []byte - info os.FileInfo -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time -} - -func (fi bindataFileInfo) Name() string { - return fi.name -} -func (fi bindataFileInfo) Size() int64 { - return fi.size -} -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} -func (fi bindataFileInfo) IsDir() bool { - return false -} -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var __000001_initDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xb6\xe6\x02\x04\x00\x00\xff\xff\x2d\x73\xd0\xe1\x1e\x00\x00\x00") - -func _000001_initDownSqlBytes() ([]byte, error) { - return bindataRead( - __000001_initDownSql, - "000001_init.down.sql", - ) -} - -func _000001_initDownSql() (*asset, error) { - bytes, err := _000001_initDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000001_init.down.sql", size: 30, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000001_initUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\xd0\x5d\x6f\xb2\x30\x14\x07\xf0\x6b\xf8\x14\xe7\x86\x00\x09\x7a\xf3\x3c\x31\x8b\xbb\xaa\x5a\x27\x1b\x2f\x06\xea\xd4\xdd\x28\x4a\xd9\x9a\x81\x22\xad\xc9\x4c\xd3\xef\xbe\xe0\xd0\x38\x75\x77\x3d\xa7\xcd\xaf\xe7\xfc\xfb\x11\x46\x04\x03\x41\x3d\x0f\x83\x3b\x84\x20\x24\x80\x67\x6e\x4c\x62\x90\xb2\x5d\x56\x34\x63\x5f\x4a\xad\xf2\xed\xfa\x93\x83\xa5\x6b\x2c\x85\x57\x14\xf5\x47\x28\xb2\xfe\x75\x6c\x47\xd7\xa4\x64\x19\xb4\xcb\x2d\x17\xef\x15\xe5\x4a\xb1\x0d\xa7\x95\x58\x24\x02\x88\xeb\xe3\x98\x20\x7f\x4c\xde\x8e\x6c\x30\xf1\x3c\x18\xe0\x21\x9a\x78\x04\x82\x70\x6a\xd9\x8e\x94\x74\x93\x2a\x75\x52\xf8\x2e\x67\x82\x5e\x1a\x03\x44\x70\xed\xdc\x00\x56\x4c\xa2\x61\x7d\x63\x99\xc6\xbc\x65\x14\x2d\x23\x05\x63\xd4\x35\xfc\xae\x91\x99\x0e\x98\x41\x38\x35\xed\x9b\x0f\x8a\x03\xdf\xe5\xf7\x7c\xab\x63\xdf\x9f\xb1\x73\x61\x94\x49\x45\x37\x62\xf1\x47\x04\x8d\xbd\xe4\xeb\x0f\x5a\x24\x4b\x29\x69\xce\xa9\x52\x3f\x65\x63\x40\xcf\x7d\x72\x03\xe2\xe8\x9a\x38\x94\x14\x08\x9e\x1d\xcf\x4c\xe4\xe7\x22\x63\x34\x4f\x39\x5c\xc7\xfa\x1c\x87\xc1\x89\xac\x5f\x36\xa0\xa3\x6b\xeb\x8a\x26\x82\xd6\xcb\x9c\xf1\x7d\x99\x5e\xb7\x52\x9a\xd3\xab\xd6\x38\x72\x7d\x14\xcd\xe1\x05\xcf\xc1\x62\xa9\x03\xe7\x58\x6c\xdd\x86\x5f\x4b\x9d\x12\xa9\x97\x46\x7d\x82\x23\x88\x31\x81\xbd\xc8\x1e\x8a\xd5\xff\x66\x94\x47\xfd\x3b\x00\x00\xff\xff\x60\xc4\xab\x56\x4c\x02\x00\x00") - -func _000001_initUpSqlBytes() ([]byte, error) { - return bindataRead( - __000001_initUpSql, - "000001_init.up.sql", - ) -} - -func _000001_initUpSql() (*asset, error) { - bytes, err := _000001_initUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000001_init.up.sql", size: 588, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000002_system_settings_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x2d\xae\x2c\x2e\x49\xcd\x8d\x2f\x4e\x2d\x29\xc9\xcc\x4b\x2f\xb6\xe6\x02\x04\x00\x00\xff\xff\xd2\x63\x5d\x39\x27\x00\x00\x00") - -func _000002_system_settings_tableDownSqlBytes() ([]byte, error) { - return bindataRead( - __000002_system_settings_tableDownSql, - "000002_system_settings_table.down.sql", - ) -} - -func _000002_system_settings_tableDownSql() (*asset, error) { - bytes, err := _000002_system_settings_tableDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000002_system_settings_table.down.sql", size: 39, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000002_system_settings_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x14\xcc\xc1\x6a\x83\x30\x18\x07\xf0\xb3\x79\x8a\xff\x51\x41\xc4\xc1\x0e\x83\x9d\x32\xf7\xc9\x64\x6e\x2d\xf1\x6b\xd1\x53\x69\x31\x96\x80\x4a\xdb\xc4\x52\x09\x79\xf7\xd2\x17\xf8\x15\x8a\x24\x13\x58\x7e\xd5\x84\xaa\xc4\xff\x86\x41\x6d\xd5\x70\x03\xef\xb3\xcb\x4d\x0f\xe6\x11\x82\x5d\xad\xd3\xd3\xc1\x6a\xe7\xcc\x7c\xb6\x88\x45\x64\x7a\xec\xa5\x2a\x7e\xa4\x8a\xdf\xf2\x3c\x49\x45\x74\x3f\x8e\x8b\x06\x53\xcb\xa9\x88\xb6\xaa\xfa\x93\xaa\xc3\x2f\x75\x88\x4d\x9f\x88\x04\xde\x9b\x01\xd9\xb4\xda\xeb\x18\xc2\x37\x95\x72\x57\x33\x5e\x80\x2c\x98\x14\x1a\x62\x2c\x6e\xf8\x98\x4e\xef\xde\xeb\xb9\x0f\xe1\x53\x3c\x03\x00\x00\xff\xff\x3e\xa0\x26\x35\x9e\x00\x00\x00") - -func _000002_system_settings_tableUpSqlBytes() ([]byte, error) { - return bindataRead( - __000002_system_settings_tableUpSql, - "000002_system_settings_table.up.sql", - ) -} - -func _000002_system_settings_tableUpSql() (*asset, error) { - bytes, err := _000002_system_settings_tableUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000002_system_settings_table.up.sql", size: 158, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000003_blocks_rootidDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xe6\x72\x09\xf2\x0f\x50\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xca\xcf\x2f\x89\xcf\x4c\xb1\xe6\x02\x04\x00\x00\xff\xff\x51\xe5\xe2\x3a\x33\x00\x00\x00") - -func _000003_blocks_rootidDownSqlBytes() ([]byte, error) { - return bindataRead( - __000003_blocks_rootidDownSql, - "000003_blocks_rootid.down.sql", - ) -} - -func _000003_blocks_rootidDownSql() (*asset, error) { - bytes, err := _000003_blocks_rootidDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000003_blocks_rootid.down.sql", size: 51, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000003_blocks_rootidUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xe6\x72\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xca\xcf\x2f\x89\xcf\x4c\x51\x08\x73\x0c\x72\xf6\x70\x0c\xd2\x30\x36\xd3\xb4\xe6\x02\x04\x00\x00\xff\xff\xc2\x68\x66\x83\x3e\x00\x00\x00") - -func _000003_blocks_rootidUpSqlBytes() ([]byte, error) { - return bindataRead( - __000003_blocks_rootidUpSql, - "000003_blocks_rootid.up.sql", - ) -} - -func _000003_blocks_rootidUpSql() (*asset, error) { - bytes, err := _000003_blocks_rootidUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000003_blocks_rootid.up.sql", size: 62, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000004_auth_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x2d\x2d\x4e\x2d\x2a\xb6\xe6\xc2\x2e\x59\x9c\x5a\x5c\x9c\x99\x9f\x57\x6c\xcd\x05\x08\x00\x00\xff\xff\xb6\xc1\x44\xa1\x3d\x00\x00\x00") - -func _000004_auth_tableDownSqlBytes() ([]byte, error) { - return bindataRead( - __000004_auth_tableDownSql, - "000004_auth_table.down.sql", - ) -} - -func _000004_auth_tableDownSql() (*asset, error) { - bytes, err := _000004_auth_tableDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000004_auth_table.down.sql", size: 61, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000004_auth_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x91\x4d\x4f\x32\x31\x14\x85\xd7\xcc\xaf\xb8\x4b\x48\x08\xe1\x25\x2f\x89\x89\xab\x82\x45\x47\x11\x4c\xa7\x1a\x58\x4d\x2a\xbd\xa3\x8d\xf3\x65\x6f\xf1\x23\x4d\xff\xbb\x99\x48\x24\x61\x30\x71\xa1\x5d\x3e\xa7\xed\xb9\xf7\x9c\xa9\xe0\x4c\x72\x90\x6c\x32\xe7\x10\xcf\x60\xb1\x94\xc0\x57\x71\x22\x13\xf0\x7e\x50\x5b\xcc\xcc\x5b\x08\x5b\x42\x4b\xd0\x8d\x3a\x46\xc3\x1d\x13\xd3\x0b\x26\xba\xff\x86\xc3\x5e\x3f\xea\x34\x52\xa9\x0a\x3c\xe4\x58\x28\x93\x7f\xc1\xd1\x78\xdc\xc0\x5a\x11\xbd\x56\xb6\xf5\x49\x91\xa9\x94\x70\x63\xd1\x1d\x2a\x6a\xeb\x1e\x53\x42\xfb\x62\x36\x7b\x8b\xd1\x5e\xd2\xca\xa9\x96\x8b\xad\x6a\x82\xcf\xe3\xbd\xc9\x60\x50\x57\xe4\x1e\x2c\x52\x08\x97\xc9\x72\xe1\x3d\xe6\x84\x21\x48\xbe\x92\xde\x63\xa9\x43\xe8\x47\x9d\x8d\x45\xe5\x30\x55\xae\x79\x36\x89\xcf\xe3\x85\x6c\xd6\xab\xf5\x11\xaa\x31\xc7\x36\xbd\x11\xf1\x35\x13\x6b\xb8\xe2\x6b\xe8\x1a\xdd\x8b\x7a\x3b\xfb\xe2\x9d\x9e\xf3\x10\xce\xf8\x8c\xdd\xce\x25\x34\xb3\xb2\xa9\xe4\x02\x12\x2e\x61\xeb\xb2\x93\xe2\xfe\xff\x6e\x90\xd3\x28\xfa\x59\x25\x84\x44\xa6\x2a\xbf\x69\xc5\x55\x4f\x58\x1e\xab\x2a\x6d\xdf\xfd\xfb\xb8\x7e\x27\x98\x8f\x00\x00\x00\xff\xff\x43\xa6\x60\x6a\xab\x02\x00\x00") - -func _000004_auth_tableUpSqlBytes() ([]byte, error) { - return bindataRead( - __000004_auth_tableUpSql, - "000004_auth_table.up.sql", - ) -} - -func _000004_auth_tableUpSql() (*asset, error) { - bytes, err := _000004_auth_tableUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000004_auth_table.up.sql", size: 683, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000005_blocks_modifiedbyDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xe6\x72\x09\xf2\x0f\x50\x70\xf6\xf7\x09\xf5\xf5\x53\xc8\xcd\x4f\xc9\x4c\xcb\x4c\x4d\x89\x4f\xaa\xb4\xe6\x02\x04\x00\x00\xff\xff\x6a\xfe\x38\x0a\x37\x00\x00\x00") - -func _000005_blocks_modifiedbyDownSqlBytes() ([]byte, error) { - return bindataRead( - __000005_blocks_modifiedbyDownSql, - "000005_blocks_modifiedby.down.sql", - ) -} - -func _000005_blocks_modifiedbyDownSql() (*asset, error) { - bytes, err := _000005_blocks_modifiedbyDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000005_blocks_modifiedby.down.sql", size: 55, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000005_blocks_modifiedbyUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xe6\x72\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\xc8\xcd\x4f\xc9\x4c\xcb\x4c\x4d\x89\x4f\xaa\x54\x08\x73\x0c\x72\xf6\x70\x0c\xd2\x30\x36\xd3\xb4\xe6\x02\x04\x00\x00\xff\xff\x30\x55\xd2\xd8\x42\x00\x00\x00") - -func _000005_blocks_modifiedbyUpSqlBytes() ([]byte, error) { - return bindataRead( - __000005_blocks_modifiedbyUpSql, - "000005_blocks_modifiedby.up.sql", - ) -} - -func _000005_blocks_modifiedbyUpSql() (*asset, error) { - bytes, err := _000005_blocks_modifiedbyUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000005_blocks_modifiedby.up.sql", size: 66, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000006_sharing_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x2d\xce\x48\x2c\xca\xcc\x4b\xb7\xe6\x02\x04\x00\x00\xff\xff\x7a\x74\xe5\xab\x1f\x00\x00\x00") - -func _000006_sharing_tableDownSqlBytes() ([]byte, error) { - return bindataRead( - __000006_sharing_tableDownSql, - "000006_sharing_table.down.sql", - ) -} - -func _000006_sharing_tableDownSql() (*asset, error) { - bytes, err := _000006_sharing_tableDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000006_sharing_table.down.sql", size: 31, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000006_sharing_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xcc\x41\x4b\xc3\x30\x18\x06\xe0\x73\xf3\x2b\xde\x63\x0b\x65\x4c\x14\x11\x3c\xa5\xf5\x9b\x06\x6b\x2b\xe9\xa7\xb8\xd3\x68\x49\xaa\xc1\xb5\x9b\x5b\x06\x8e\x90\xff\x2e\xbd\x78\xd8\xfd\xe1\x29\x35\x49\x26\xb0\x2c\x2a\x82\x5a\xa1\x6e\x18\xf4\xa1\x5a\x6e\x11\xc2\x62\x7f\xb0\x83\xfb\x8d\xf1\xf8\xd5\x1d\xdc\xf4\x89\x54\x24\xce\xe0\x5d\xea\xf2\x49\xea\xf4\xfa\x36\xcb\x45\x62\xa7\xae\xdf\x5a\x83\xa2\x69\x2a\x92\x75\x2e\x12\xbf\xfb\xb6\xd3\xbf\xba\x5a\x2e\x67\x36\xee\x8c\x1b\x9c\x35\x9b\xfe\x7c\x11\x9c\xf6\xa6\xf3\x76\xd3\x79\x14\xea\x51\xd5\x9c\x8b\xe4\x55\xab\x17\xa9\xd7\x78\xa6\x35\x52\x67\x32\x91\x21\x04\x37\x60\x31\x9e\x8f\x3f\xdb\x18\x1f\x68\x25\xdf\x2a\xc6\xbc\xc8\x92\x49\xa3\x25\xc6\xc9\x0f\x77\x63\x7f\x13\x82\x9d\x4c\x8c\xf7\xe2\x2f\x00\x00\xff\xff\x82\xbb\xda\xde\xdc\x00\x00\x00") - -func _000006_sharing_tableUpSqlBytes() ([]byte, error) { - return bindataRead( - __000006_sharing_tableUpSql, - "000006_sharing_table.up.sql", - ) -} - -func _000006_sharing_tableUpSql() (*asset, error) { - bytes, err := _000006_sharing_tableUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000006_sharing_table.up.sql", size: 220, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000007_workspaces_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x2d\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x2d\xb6\xe6\x02\x04\x00\x00\xff\xff\x1a\xe4\xe6\x36\x22\x00\x00\x00") - -func _000007_workspaces_tableDownSqlBytes() ([]byte, error) { - return bindataRead( - __000007_workspaces_tableDownSql, - "000007_workspaces_table.down.sql", - ) -} - -func _000007_workspaces_tableDownSql() (*asset, error) { - bytes, err := _000007_workspaces_tableDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000007_workspaces_table.down.sql", size: 34, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000007_workspaces_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xcd\x41\x4b\xc3\x30\x18\xc6\xf1\x73\xf3\x29\xde\xe3\x0a\x65\x4c\x14\x11\x3c\x65\x35\xd3\x6a\xed\x24\xcd\x64\x3b\x95\xce\xbc\x2d\x61\x6b\x1b\x9b\x14\x1d\xe1\xfd\xee\x32\x19\x1e\x3c\x3f\x0f\xff\x5f\x2a\x05\x57\x02\x14\x5f\xe6\x02\xb2\x15\x14\x6b\x05\x62\x9b\x95\xaa\x84\x10\xe6\x76\xc4\xc6\x7c\x13\x7d\x0d\xe3\xc1\xd9\xfa\x03\x1d\xcc\x58\x64\x34\xbc\x73\x99\x3e\x71\x39\xbb\xbe\x8d\x13\x16\x39\xd3\xf6\x93\xad\xfc\x70\xc0\xfe\x6f\xba\x5a\x2c\xe2\xdf\x5c\xb1\xc9\xf3\xf3\x09\xbd\x37\x7d\xeb\x20\x04\xd3\xc0\xdc\x0e\xce\xb7\x23\x3a\xa2\xe7\x72\x5d\x84\x80\x47\x87\x44\x4a\x6c\x55\x08\xd8\x6b\xa2\x84\x45\xdd\xa0\x4d\x63\x50\x57\xfb\xd3\x3f\x71\xb2\xba\xf6\x58\xd5\x1e\x96\xd9\x63\x56\xa8\x84\x45\x6f\x32\x7b\xe5\x72\x07\x2f\x62\x07\x33\xa3\x63\x16\x5f\xa4\xee\xe4\x3e\x8f\x44\x0f\x62\xc5\x37\xb9\x82\x73\x85\xa7\x4a\x48\x28\x85\x82\xc9\x37\x77\xdd\xfe\xe6\x62\xde\xb3\x9f\x00\x00\x00\xff\xff\x29\x38\x4d\xc3\x10\x01\x00\x00") - -func _000007_workspaces_tableUpSqlBytes() ([]byte, error) { - return bindataRead( - __000007_workspaces_tableUpSql, - "000007_workspaces_table.up.sql", - ) -} - -func _000007_workspaces_tableUpSql() (*asset, error) { - bytes, err := _000007_workspaces_tableUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000007_workspaces_table.up.sql", size: 272, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000008_teamsDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xe6\x72\x09\xf2\x0f\x50\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x8d\xcf\x4c\xb1\xe6\xe2\xc2\xa1\xb1\x38\x23\xb1\x28\x33\x2f\x9d\x1c\x9d\xa9\xc5\xc5\x99\xf9\x79\xa8\x96\x26\x96\x96\x64\xc4\x17\xa7\x16\x95\x65\x26\xa7\x5a\x73\x01\x02\x00\x00\xff\xff\x24\x48\xc4\xb6\xad\x00\x00\x00") - -func _000008_teamsDownSqlBytes() ([]byte, error) { - return bindataRead( - __000008_teamsDownSql, - "000008_teams.down.sql", - ) -} - -func _000008_teamsDownSql() (*asset, error) { - bytes, err := _000008_teamsDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000008_teams.down.sql", size: 173, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000008_teamsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xe6\x72\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x8d\xcf\x4c\x51\x08\x73\x0c\x72\xf6\x70\x0c\xd2\x30\x36\xd3\xb4\xe6\xe2\xc2\x61\x46\x71\x46\x62\x51\x66\x5e\x3a\x85\x86\xa4\x16\x17\x67\xe6\xe7\xa1\x38\x25\xb1\xb4\x24\x23\xbe\x38\xb5\xa8\x2c\x33\x39\x15\x6e\x8a\x91\x01\xc8\x94\xd0\x00\x17\xc7\x10\x2c\x3e\x51\x08\x76\x0d\x41\xb5\xdd\x56\x41\xdd\x40\x5d\x21\xdc\xc3\x35\xc8\x15\x43\x42\x5d\xc1\x3f\x08\x55\xd0\x33\x58\xc1\x2f\xd4\xc7\xc7\x9a\x0b\x10\x00\x00\xff\xff\xab\x8d\x48\xa9\x30\x01\x00\x00") - -func _000008_teamsUpSqlBytes() ([]byte, error) { - return bindataRead( - __000008_teamsUpSql, - "000008_teams.up.sql", - ) -} - -func _000008_teamsUpSql() (*asset, error) { - bytes, err := _000008_teamsUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000008_teams.up.sql", size: 304, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000009_blocks_historyDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\xb6\xe6\x72\xf4\x09\x71\x0d\xc2\x25\x1d\x9f\x91\x59\x5c\x92\x5f\x54\xa9\x10\xe4\xea\xe7\xe8\xeb\xaa\x10\xe2\x8f\xcd\x08\x40\x00\x00\x00\xff\xff\x38\xe5\xec\x7a\x61\x00\x00\x00") - -func _000009_blocks_historyDownSqlBytes() ([]byte, error) { - return bindataRead( - __000009_blocks_historyDownSql, - "000009_blocks_history.down.sql", - ) -} - -func _000009_blocks_historyDownSql() (*asset, error) { - bytes, err := _000009_blocks_historyDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000009_blocks_history.down.sql", size: 97, mode: os.FileMode(436), modTime: time.Unix(1621275260, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000009_blocks_historyUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x93\xd1\x6f\xda\x30\x10\xc6\x9f\xc9\x5f\x71\x2f\x11\xc9\x94\x56\x93\x36\xa1\xa9\x48\x93\x42\x38\xc0\x5b\x62\x57\x8e\xbb\x96\xbd\x50\x20\xce\xb0\x1a\x48\x1a\xa7\xea\x50\x94\xff\x7d\x4a\x47\x28\x0d\xa0\x3d\x4c\x7d\xc3\x3e\xdf\xef\xbe\xfb\xf8\xe2\xfa\x02\x39\x08\x77\xe0\x23\x94\xe5\x65\x96\xcb\x58\xfd\xae\xaa\x45\x92\x2e\x1f\x34\x70\xa4\x6e\x80\x20\xd8\x71\x6d\xb6\x52\xba\x48\xf3\x6d\xdf\xf0\x38\xba\x02\x77\x0c\x32\x02\xca\x04\xe0\x1d\x09\x45\x78\x82\x68\x19\x1d\x15\xc1\x0f\x97\x7b\x13\x97\x5b\x9f\x7a\xb6\x63\x74\xca\x52\xc5\x70\x99\xa5\xba\xf8\x95\x4b\x5d\x55\x6a\xa3\x65\x5e\xcc\xe6\x05\x08\x12\x60\x28\xdc\xe0\x5a\xfc\x7c\xc1\xd2\x1b\xdf\x87\x21\x8e\xdc\x1b\x5f\x00\x65\xb7\x96\xed\x94\xa5\xdc\x44\x55\xd5\x50\xf4\x63\xa2\x0a\x79\xc8\x18\xba\x02\x6b\xce\x11\xc0\x0a\x05\x1f\xd5\x15\xab\x6b\x4e\x2f\xcc\xf5\x85\x19\x81\x39\xb9\x32\x83\x2b\x33\xee\x3a\xd0\xa5\xec\xb6\x6b\x1f\x0d\x58\x6f\xf5\x63\x72\x8a\x6f\xf5\xec\xd3\x1a\x7b\x07\x8c\x6c\x9e\xcb\x4d\x31\x3b\x63\xc1\x8e\x7d\xaf\x97\x2b\xb9\x9e\xdf\x97\xa5\x4c\xb4\xac\xaa\xbf\xc7\x1d\x03\x06\x64\x4c\xa8\x70\x8c\x4e\xb1\xcd\x24\x08\xbc\x7b\xf9\xad\x8a\x64\x7f\x88\x95\x4c\x22\x0d\x6d\x5b\xbf\x85\x8c\x36\xc8\xfa\xe5\x0e\xe8\x18\x9d\x65\x2e\xe7\x85\xac\x97\xd9\xc3\x9f\xb2\xa8\x7d\x15\xc9\x44\xb6\xae\xf2\x34\x3d\xb1\xcc\x3a\x8d\x54\xac\x64\x34\x5b\x6c\x5b\x95\xe7\x34\x7f\xd0\xd9\x7c\x29\x8f\x9b\xae\x39\x09\x5c\x3e\x85\xef\x38\x05\xeb\xf0\x9d\xa3\x22\xdb\xb0\xe1\x8d\x43\x8d\xbd\x75\xbf\xeb\xd5\x09\x0e\x51\xc0\x53\x11\x7f\x59\x2f\x3e\xef\xf6\xea\x1b\xc6\x9b\x1e\x83\xd0\x10\xb9\x00\x32\xa6\x8c\x23\x10\x7a\x2a\xd5\x60\x85\xe8\xa3\x27\xe0\x03\x8c\x38\x0b\xce\xc7\x1e\x18\x1f\x22\x87\xc1\x14\x0e\x92\x80\xa1\x67\xf7\x8d\xe6\xcf\x6e\xfb\xbf\x17\xf0\x4e\x93\x81\x51\xf0\x18\x1d\xf9\xc4\x13\x30\x64\x75\x18\x27\x84\x8e\xdb\x82\x9a\x2f\xa4\x91\xc3\xf8\x3f\x2c\xf9\x4f\x5d\xaf\xf3\x87\xe8\xa3\xc0\x33\x18\x78\x5e\xc9\x5c\xc2\x6b\xc8\xbe\xc2\xc7\xbe\xf1\x27\x00\x00\xff\xff\xd3\x97\xb4\x77\x9f\x04\x00\x00") - -func _000009_blocks_historyUpSqlBytes() ([]byte, error) { - return bindataRead( - __000009_blocks_historyUpSql, - "000009_blocks_history.up.sql", - ) -} - -func _000009_blocks_historyUpSql() (*asset, error) { - bytes, err := _000009_blocks_historyUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000009_blocks_history.up.sql", size: 1183, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000010_blocks_created_byDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x54\xc1\x6e\x9b\x40\x10\xbd\xf3\x15\x73\x41\x86\xca\x89\x2a\xb5\xb2\xaa\x58\xaa\x84\x61\x6c\xd3\xc2\xae\xb5\x6c\x9a\xb8\x17\xc7\x36\x4b\x8d\x82\x0d\x01\xa2\xd4\x42\xfc\x7b\x85\x0d\x0e\x8e\xb1\xa3\xaa\xce\xa5\xca\x71\x77\x67\xde\x7b\x33\x3b\xf3\x34\x8b\x23\x03\xae\xf5\x2c\x84\x2c\xbb\x8c\x62\xe1\xf9\xbf\xf3\x7c\x16\x84\xf3\xfb\x04\x18\x12\xcd\x46\xe0\xf4\xf0\x6d\x12\x06\x6e\x57\xd2\x19\x6a\x1c\xcb\x7c\xb3\x0f\x84\x72\xc0\x5b\xd3\xe1\x4e\x03\x9a\x22\x01\x00\xf8\x2e\xfc\xd0\x98\x3e\xd4\x98\xf2\xa9\xa3\xb6\x37\x77\x59\xe6\x7b\x70\x19\x85\x49\xfa\x2b\x16\x49\x9e\xfb\xab\x44\xc4\xe9\x64\x9a\x02\x37\x6d\x74\xb8\x66\x8f\xf8\xcf\x0d\x38\xb9\xb6\x2c\x30\xb0\xaf\x5d\x5b\x1c\x08\xbd\x51\xd4\x76\x96\x89\x95\x9b\xe7\x35\xa0\xe4\x21\xf0\x53\x51\x87\x31\x34\x8e\x05\xd4\x01\x86\xe2\x70\xd6\x2f\x5e\x94\x96\x3c\xbe\x90\x97\x17\xb2\x0b\xf2\xf0\x4a\xb6\xaf\x64\xaf\xd5\x86\x16\xa1\x37\x2d\xb5\x89\x63\xb9\x4e\x1e\x82\x26\x0a\xa5\xa3\x36\x2b\xed\xec\xc3\x44\xd3\x58\xac\xd2\xc9\xf1\x76\x94\x0c\x77\xc9\x7c\x21\x96\xd3\xbb\x2c\x13\x41\x22\xf2\x7c\x7b\x2c\x91\xa0\x67\x0e\x4c\xc2\xb7\x69\xe9\x3a\x12\xc0\xf1\xb6\x3a\xfa\x69\x50\x3f\x7b\xbe\x08\xdc\xe4\xa0\xd7\xdf\x1c\x4a\x2a\xec\x22\xb8\x44\xde\xe6\xcc\x63\x31\x4d\x45\x51\x5e\x9d\xe8\x31\x72\x1b\x6e\x5d\x11\x88\xc3\xdb\x38\x0c\x9b\x8b\x5c\x86\xae\xef\xf9\xc2\x9d\xcc\xd6\x87\x8f\x4f\x61\x7c\x9f\x44\xd3\xb9\x68\x4c\x1d\x31\xd3\xd6\xd8\x18\xbe\xe3\x18\x94\x7a\x68\xdb\x77\xd5\x4d\x84\xba\xdf\xc3\xea\x1b\x0a\x18\x4d\x2f\x26\xde\x41\x0e\x8f\xa9\xf7\x65\x39\xfb\x5c\x16\xdc\x95\xa4\xbd\x1c\xc9\x24\x0e\x32\x0e\xe6\x80\x50\x86\x60\x92\xa6\x2d\x00\xc5\x41\x0b\x75\x0e\x1f\xa0\xcf\xa8\xdd\xbc\x26\x40\x99\x81\x0c\x7a\x63\xa8\x4d\x0b\x3a\xba\xda\x95\xaa\x81\x78\xf9\x29\x3b\xf2\x37\x60\x05\x4a\x40\xa7\xa4\x6f\x99\x3a\x07\x83\x16\xc3\x3a\x34\xc9\xe0\xa5\x98\x6a\x89\x2a\x29\x94\xbd\xd2\x8a\x7f\xd0\xf4\xcc\x6d\xa0\x85\x1c\x8f\x40\xc0\xd3\x42\xc4\xa2\x36\x68\x5f\xe1\x63\x57\x32\x18\x1d\x1d\x73\xb0\xad\x4b\x49\xd2\x49\x9b\x9b\x2c\xfc\x24\x0d\xe3\xf5\x49\xbb\x2b\x63\xfe\xde\xf6\x76\xe8\xef\xf6\xf7\x6e\x7f\xff\x99\xfd\x3d\xcf\xf6\x6b\xcb\x5f\x5b\x9f\xb3\xdb\xe1\x79\x55\xbc\x91\x3d\xee\x44\x9e\x41\x63\xcd\x2e\x4f\x79\xdf\x9e\x65\xfd\x09\x00\x00\xff\xff\x5f\xbd\x27\xe2\xe9\x09\x00\x00") - -func _000010_blocks_created_byDownSqlBytes() ([]byte, error) { - return bindataRead( - __000010_blocks_created_byDownSql, - "000010_blocks_created_by.down.sql", - ) -} - -func _000010_blocks_created_byDownSql() (*asset, error) { - bytes, err := _000010_blocks_created_byDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000010_blocks_created_by.down.sql", size: 2537, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000010_blocks_created_byUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x8e\x41\x4b\xc4\x30\x10\x85\xef\xfd\x15\x73\x6b\x0a\xb2\x20\x82\x97\x65\x0f\xb3\x49\x44\x21\x6e\x25\x4d\x05\x4f\x65\xb7\x9d\xb2\xc1\xc6\x48\x12\xd0\x52\xfa\xdf\xa5\x78\x11\xd4\xb2\x97\x39\xcc\x7b\xef\xe3\x43\x65\xa4\x06\x83\x7b\x25\x61\x9a\x36\xef\x81\x7a\xfb\x39\xcf\xa7\xc1\xb7\xaf\x11\x50\x08\xe0\xa5\xaa\x1f\x0f\xd0\x06\x3a\x26\xea\x9a\xd3\x08\xcf\xa8\xf9\x3d\x6a\x76\x73\x5b\x6c\xb3\x55\x40\x73\xb6\x31\xf9\x30\x5e\x02\xca\xea\x27\x81\xe6\x2f\x8b\x4a\x9a\x9f\xab\x1d\xf0\x12\x95\xac\xb8\x64\x87\x5a\xa9\x87\x3b\xc6\x22\x0d\xd4\x26\x70\xbe\xb3\xbd\xfd\x6e\xf5\xc1\xbb\x15\xa1\x8f\x33\x05\xfa\x3f\xdf\xd8\x0e\x76\xbf\xe3\xe5\x5d\x6a\x21\x35\xec\x5f\xd6\xc6\x6f\x91\x42\x6a\x8e\x09\xb0\xe2\x30\x58\x67\x13\x5c\x17\x57\x90\xe7\xcb\x89\x63\x4c\xe4\xf2\x62\x9b\x7d\x05\x00\x00\xff\xff\xcb\x4a\xd7\xe3\x7d\x01\x00\x00") - -func _000010_blocks_created_byUpSqlBytes() ([]byte, error) { - return bindataRead( - __000010_blocks_created_byUpSql, - "000010_blocks_created_by.up.sql", - ) -} - -func _000010_blocks_created_byUpSql() (*asset, error) { - bytes, err := _000010_blocks_created_byUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000010_blocks_created_by.up.sql", size: 381, mode: os.FileMode(436), modTime: time.Unix(1625794710, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000011_match_collationDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x04\xc0\x31\x0e\x85\x20\x0c\x06\xe0\x9d\x53\xfc\x17\xe8\xf0\xe6\x37\x1a\x36\xe3\x22\x17\x80\x58\xa1\xb1\x69\x8d\x74\xf1\xf6\x7e\x44\xd8\x3c\x86\x58\x47\x38\x1a\xe3\x70\x63\x0c\x7e\x38\x11\xa1\x0c\x99\xb8\x6b\x67\xc8\x84\x58\xb0\x85\xb8\x55\xd5\x17\xca\x67\xa0\x69\xb5\x2b\xed\x79\xcd\x4b\xc1\xef\x9f\xbe\x00\x00\x00\xff\xff\x54\x6c\x45\x67\x4e\x00\x00\x00") - -func _000011_match_collationDownSqlBytes() ([]byte, error) { - return bindataRead( - __000011_match_collationDownSql, - "000011_match_collation.down.sql", - ) -} - -func _000011_match_collationDownSql() (*asset, error) { - bytes, err := _000011_match_collationDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000011_match_collation.down.sql", size: 78, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000011_match_collationUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x95\xcd\x6e\xda\x40\x14\x85\xf7\x3c\xc5\x51\x36\x80\xd4\x58\xea\x3a\x42\xaa\x63\xa6\xea\xc2\x0d\x29\xb8\x4a\x77\x68\x62\xae\xf1\x28\xf3\x43\xe6\x8e\x45\x91\xe5\x77\xaf\x3a\xc6\x01\x55\xd9\x51\x29\x6c\xed\x73\xbe\x3b\xdf\xcc\xe2\xb6\xad\xaa\x20\xed\x06\x89\x39\xf0\xab\x46\xb2\xd3\xcd\x56\xd9\xae\x1b\x01\xc0\xed\x2d\x4a\xa7\xb5\x0c\xca\x59\xb8\x0a\x46\x86\x40\xde\x38\x0e\x63\x46\x56\x4b\x6b\x49\x33\x82\x7c\xd6\x14\xf3\x2b\x51\xe0\xcb\x29\x94\xbd\x75\x67\x98\xac\x44\x2e\xb2\xa2\x0f\xaf\x4f\xd4\xca\x3b\x03\x65\x2b\xe7\x4d\xfc\xb0\xe6\xb2\x26\x23\x93\x98\x63\x3c\x7d\x13\x4b\x71\x2c\x59\x69\x08\x33\x8c\x87\xc1\x63\xa4\x0f\xf3\xe3\xbf\xbe\x75\x36\x67\x9e\x16\xe9\x7d\xba\x12\x93\xe9\x74\x7a\x37\x1a\x6c\x9e\xb5\x2b\x5f\xf8\x74\xd6\x66\xb7\x91\x81\xde\xce\xf9\xa3\x21\x7f\xc0\x0c\xd9\xe2\x21\x4b\x8b\xc9\x38\xcd\x0b\xb1\x44\x91\xde\xe7\x02\x6d\x9b\xec\x3c\x55\xea\x77\xd7\xf5\x14\x64\x8b\x3c\x4f\x0b\x81\xf1\xa7\x77\xa5\xa7\x77\x71\xce\xe3\x52\x3c\xa6\x4b\x01\x0e\x26\xe0\xeb\x72\xf1\xfd\xfd\xa9\x7d\x58\xfc\x12\xd9\xcf\xa2\x0f\xf7\x5f\xe6\x22\xcd\xf3\x45\xf6\x77\xce\x39\xe9\x5f\x25\xd4\x8a\x83\xf3\x87\xff\xa3\xb6\x3e\xd2\xae\x44\x91\x89\x59\x39\x7b\xf1\xbb\x0d\x9c\x6b\xd1\xaa\xa5\x57\x76\x7b\xb1\x55\x8f\xb9\x16\xa9\x03\x07\x32\x60\x0a\x41\xd9\xed\xe5\x4f\x16\x71\xeb\x01\x77\x25\x92\x0d\x93\xbf\x58\x2d\x42\xae\x44\x68\xef\xfc\x0b\xef\x64\x49\x17\x5b\x9d\x48\x1f\xac\xd6\xb6\xa4\x99\x4e\x8b\xec\x89\x60\x89\x36\x90\x78\x8d\x26\x35\x79\x82\x0b\x35\xf9\xbd\x62\x42\xa8\x09\x46\x6d\x7d\xbf\x95\xf6\x4a\x6b\x78\xe2\x46\x87\xa1\xaf\x2c\xa4\x05\x99\x5d\x38\x1c\x09\xfb\x9a\x6c\xec\xa9\x0a\xa5\xb3\x1b\x15\xab\x8a\x51\x49\xcd\x94\x0c\x45\x71\x56\x29\x65\xc3\xc4\x90\xb8\xe9\xaf\x73\x2f\xb9\x47\xde\x80\xbc\x77\x3e\x39\x5e\x7f\x5c\x61\x9f\xa3\x84\xdd\x74\xdd\xe8\x4f\x00\x00\x00\xff\xff\xed\x3f\x00\x0b\xa7\x07\x00\x00") - -func _000011_match_collationUpSqlBytes() ([]byte, error) { - return bindataRead( - __000011_match_collationUpSql, - "000011_match_collation.up.sql", - ) -} - -func _000011_match_collationUpSql() (*asset, error) { - bytes, err := _000011_match_collationUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000011_match_collation.up.sql", size: 1959, mode: os.FileMode(436), modTime: time.Unix(1631562120, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000012_match_column_collationDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x04\xc0\x31\x0e\x85\x20\x0c\x06\xe0\x9d\x53\xfc\x17\xe8\xf0\xe6\x37\x1a\x36\xe3\x22\x17\x80\x58\xa1\xb1\x69\x8d\x74\xf1\xf6\x7e\x44\xd8\x3c\x86\x58\x47\x38\x1a\xe3\x70\x63\x0c\x7e\x38\x11\xa1\x0c\x99\xb8\x6b\x67\xc8\x84\x58\xb0\x85\xb8\x55\xd5\x17\xca\x67\xa0\x69\xb5\x2b\xed\x79\xcd\x4b\xc1\xef\x9f\xbe\x00\x00\x00\xff\xff\x54\x6c\x45\x67\x4e\x00\x00\x00") - -func _000012_match_column_collationDownSqlBytes() ([]byte, error) { - return bindataRead( - __000012_match_column_collationDownSql, - "000012_match_column_collation.down.sql", - ) -} - -func _000012_match_column_collationDownSql() (*asset, error) { - bytes, err := _000012_match_column_collationDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000012_match_column_collation.down.sql", size: 78, mode: os.FileMode(436), modTime: time.Unix(1633447591, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000012_match_column_collationUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x96\xcf\x6e\xe2\x3c\x14\xc5\xf7\x7d\x8a\xab\x6e\x52\xa4\x16\xe9\x5b\x57\x48\x9f\x9b\x7a\xd4\x45\x0a\x9d\x90\x4e\x67\x87\x5c\xb8\x21\x56\xfd\x27\xf5\x75\x86\x41\x88\x77\x1f\x61\x27\x04\x75\x3a\x2b\xba\xc9\x12\x73\xce\xef\xfa\x1c\x67\x71\x77\x3b\x59\x82\x30\x2b\x18\xeb\x2d\xbd\x2b\x18\xd7\xaa\x59\x4b\xb3\xdf\x5f\x00\x00\xdc\xdc\x80\xaf\x24\x81\x96\x6b\x27\xbc\xb4\x06\x44\x5d\x2b\x89\x04\x4b\xab\x54\x3c\xb1\xe6\xf0\xa3\xd1\x06\x14\xfe\x42\x35\xee\x8c\x27\x8a\x12\xb4\xf0\x1e\x9d\xb6\xe4\x13\x82\xb4\x12\xc6\xa0\x22\xf0\xe2\x55\x61\xd0\xcf\x79\x01\xff\xf7\xa2\xf4\xe8\x9d\xc0\xd5\x9c\x67\x3c\x2d\xa2\x78\xd1\x53\x4b\x67\x35\x48\x53\x5a\xa7\xc3\xc1\x82\x96\x15\x6a\x31\x0e\x3a\x82\x97\x07\x9e\xf3\xd6\x64\x84\x46\x98\x40\xd2\x0d\x4e\x80\x4d\xef\xdb\xff\xa2\xeb\x64\xce\x3d\x2b\xd8\x1d\x9b\xf3\xab\xd1\x68\x74\x7b\x0c\x53\x09\x47\xe8\xff\x8e\xf2\x70\x1a\x25\x21\x98\x1e\x46\xc5\x3e\x3e\x0d\xd6\x72\xfa\x71\xe9\x03\xcb\x59\x5a\xf0\x7c\x31\xe7\xc5\x62\xca\x1e\xf9\x3f\x93\x45\xec\xd7\x44\x0b\xb2\x74\x96\x3d\x3f\x4e\xe3\xd0\x09\x24\x87\xbb\x27\xa3\xdb\x8b\x2e\xf4\xab\xb2\xcb\x37\xea\x63\x34\xf5\x4a\x78\x3c\xbe\xcd\xf7\x06\xdd\x16\x26\x90\xce\xa6\x29\x2b\xae\x12\x96\x15\x3c\x87\x82\xdd\x65\x1c\x76\xbb\x71\xed\xb0\x94\xbf\xf7\xfb\x48\x39\xa8\x7e\xf0\xbc\x80\x62\xd6\x47\x0e\xd8\xe4\xfa\x93\x82\xae\x21\x39\xdc\x2e\x63\x05\xff\x28\xe8\xc6\xb7\x8f\xf3\x94\xf3\x27\x96\x73\x20\xaf\x3d\x7c\xcb\x67\x8f\x9f\xdf\x33\x8a\xf9\x4f\x9e\x3e\x17\x51\x1c\x4f\xee\x39\xcb\xb2\x59\x7a\x98\x73\x4a\xfa\x58\x02\x54\x92\xbc\x75\xdb\xaf\x29\x63\xd1\xd2\x06\x5b\x0a\x21\x91\xb4\xe6\xec\x6f\xa3\xe3\x0c\xb7\x88\x4a\x38\x69\xd6\x67\xf7\x10\x31\xc3\xad\x61\x4b\x1e\x35\x10\x7a\x2f\xcd\xfa\xfc\xcf\x22\xe0\x16\x1d\x6e\xb0\xb5\x34\x84\xee\xec\x32\x02\x64\xb0\x15\x6c\xac\x7b\xa3\x5a\x2c\xf1\xec\x1e\x7a\xd2\xe0\xca\xd8\xed\x50\x11\xf6\x3b\xd5\x0b\x82\x41\x5c\x81\x80\xf7\x90\xbd\x42\x87\x60\x7d\x85\x6e\x23\x09\xc1\x57\x78\xb2\x71\x6d\xa4\x52\xe0\x90\x1a\xe5\x3b\xbf\x34\x20\x0c\xa0\xae\xfd\xb6\x25\x6c\x2a\x34\xc1\x27\x4b\x58\x5a\xb3\x92\xc1\x2a\x09\x4a\xa1\x08\x8f\x3b\x19\x3f\xb1\x2c\x45\x43\x48\x20\xe0\x32\x3e\xc0\x46\x50\x44\x5e\x02\x3a\x67\xdd\xb8\x7d\xb0\xb0\x39\xfc\x17\x42\x98\xd5\x7e\x7f\xf1\x27\x00\x00\xff\xff\xf5\xcb\x89\x20\x32\x0a\x00\x00") - -func _000012_match_column_collationUpSqlBytes() ([]byte, error) { - return bindataRead( - __000012_match_column_collationUpSql, - "000012_match_column_collation.up.sql", - ) -} - -func _000012_match_column_collationUpSql() (*asset, error) { - bytes, err := _000012_match_column_collationUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000012_match_column_collation.up.sql", size: 2610, mode: os.FileMode(436), modTime: time.Unix(1633447591, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000013_millisecond_timestampsDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe2\x0a\x0d\x70\x71\x0c\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x2d\x2d\x4e\x2d\x2a\x56\x08\x76\x0d\x51\x48\x2e\x4a\x4d\x2c\x49\x8d\x4f\x2c\x51\xb0\x45\xb0\xf5\x0d\x0d\x0c\x0c\x74\x14\x4a\x0b\x52\xe0\x72\x70\x36\x54\x2e\x25\x35\x27\x15\x26\x07\x67\x83\xe5\xb8\x38\xc3\x3d\x5c\x83\x5c\x91\x4c\xb6\x53\x00\x89\xc3\x81\x35\x17\x16\x17\x25\xe5\xe4\x27\x67\x0f\x46\x27\xc5\x67\x64\x16\x97\xe4\x17\x55\x0e\x2a\xa7\x95\xe7\x17\x65\x17\x17\x24\x26\xa7\x42\x42\x0c\xb7\xd5\x30\xe3\x11\x2a\x88\x31\xbe\x38\x23\xb1\x28\x33\x2f\x9d\x36\x66\xa7\x16\x17\x67\xe6\xe7\x51\x12\xd5\x44\x84\x19\x20\x00\x00\xff\xff\x27\x62\xfb\x7c\xf4\x02\x00\x00") - -func _000013_millisecond_timestampsDownSqlBytes() ([]byte, error) { - return bindataRead( - __000013_millisecond_timestampsDownSql, - "000013_millisecond_timestamps.down.sql", - ) -} - -func _000013_millisecond_timestampsDownSql() (*asset, error) { - bytes, err := _000013_millisecond_timestampsDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000013_millisecond_timestamps.down.sql", size: 756, mode: os.FileMode(436), modTime: time.Unix(1633610888, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000013_millisecond_timestampsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe2\x0a\x0d\x70\x71\x0c\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x2d\x2d\x4e\x2d\x2a\x56\x08\x76\x0d\x51\x48\x2e\x4a\x4d\x2c\x49\x8d\x4f\x2c\x51\xb0\x45\xb0\xb5\x0c\x0d\x0c\x0c\x74\x14\x4a\x0b\x52\xe0\x72\x70\x36\x54\x2e\x25\x35\x27\x15\x26\x07\x67\x83\xe5\xb8\x38\xc3\x3d\x5c\x83\x5c\x91\x4c\xb6\x51\x00\x89\xc3\x81\x35\x17\x16\x17\x25\xe5\xe4\x27\x67\x0f\x46\x27\xc5\x67\x64\x16\x97\xe4\x17\x55\x0e\x2a\xa7\x95\xe7\x17\x65\x17\x17\x24\x26\xa7\x42\x42\x0c\xb7\xd5\x30\xe3\x11\x2a\x88\x31\xbe\x38\x23\xb1\x28\x33\x2f\x9d\x36\x66\xa7\x16\x17\x67\xe6\xe7\x51\x12\xd5\x44\x84\x19\x20\x00\x00\xff\xff\x35\xca\x68\xeb\xf4\x02\x00\x00") - -func _000013_millisecond_timestampsUpSqlBytes() ([]byte, error) { - return bindataRead( - __000013_millisecond_timestampsUpSql, - "000013_millisecond_timestamps.up.sql", - ) -} - -func _000013_millisecond_timestampsUpSql() (*asset, error) { - bytes, err := _000013_millisecond_timestampsUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000013_millisecond_timestamps.up.sql", size: 756, mode: os.FileMode(436), modTime: time.Unix(1633610888, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000014_add_not_null_constraintDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\xce\x4c\x53\xd0\xcb\xad\x2c\x2e\xcc\xa9\xad\xe5\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x4d\xca\xc9\x4f\xce\x2e\x56\xf0\xf5\x77\xf1\x74\x8b\x54\x48\x2e\x4a\x4d\x2c\x49\x4d\x89\x4f\xaa\x54\x28\x4b\x2c\x4a\xce\x48\x2c\xd2\x30\x36\xd3\xb4\x26\x4e\x73\x6e\x7e\x4a\x66\x5a\x26\x16\xdd\xd5\xd5\xa9\x79\x29\xb5\xb5\x5c\x5c\x10\x17\x15\xe4\x17\x97\xa4\x17\xa5\x16\x13\x74\x14\x44\xd6\xd9\xdf\x27\xd4\xd7\x0f\xd9\x69\x2e\x41\xfe\x01\x0a\x7e\xfe\x21\x0a\x7e\xa1\x3e\x3e\x84\x1c\x87\x62\x08\xb2\x13\xd1\x4c\x81\x39\x12\x10\x00\x00\xff\xff\xf8\x47\xa8\x96\x36\x01\x00\x00") - -func _000014_add_not_null_constraintDownSqlBytes() ([]byte, error) { - return bindataRead( - __000014_add_not_null_constraintDownSql, - "000014_add_not_null_constraint.down.sql", - ) -} - -func _000014_add_not_null_constraintDownSql() (*asset, error) { - bytes, err := _000014_add_not_null_constraintDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000014_add_not_null_constraint.down.sql", size: 310, mode: os.FileMode(436), modTime: time.Unix(1635256817, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000014_add_not_null_constraintUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\xce\x3f\x6b\x83\x40\x18\xc7\xf1\xdd\x57\xf1\x6c\xb6\x8b\x4b\xa1\x4b\xe9\x60\xab\x05\xe1\xd4\x12\xcf\x21\x53\xf0\xcf\x63\x3c\xa2\xd1\xdc\x1d\x49\xe4\xb8\xf7\x9e\x21\x31\x9c\x60\x12\xe7\x1f\x7c\x7f\x9f\xf4\xdf\x73\xa9\x0f\x4a\x39\x3d\xc7\x8a\x9d\xb5\xce\x9b\xae\xd8\x09\x48\x7c\x0a\x05\xc7\x4c\x62\xb9\xc9\x07\xf8\x06\x5b\x0c\x42\x62\x6b\xc3\xa9\x46\x8e\xe6\x16\x24\x10\xa5\x84\x7c\x59\x4f\x63\x6d\x57\xb2\x8a\x3d\xa8\x99\xe3\x3d\x67\x29\xc5\x2a\x70\xda\x41\x1c\x1a\xad\x2d\x97\x50\x7f\x05\xd4\xfd\x21\x73\x17\x61\xec\x05\x7f\x6b\x93\x75\xcc\x78\x51\x67\xfc\xed\xe3\xf3\x1d\xa2\x98\xde\xa2\x8b\x2a\x26\x67\x3e\xa3\x14\xee\x4b\xad\x47\x63\xdf\x09\xb9\xe5\x28\x5e\x32\xaf\xeb\x6f\x4c\xd2\x30\x32\xb1\x02\xe5\x62\xe4\xa4\x61\x52\xa7\x91\x91\x78\x09\x00\x00\xff\xff\x17\x78\x4b\x75\xe3\x01\x00\x00") - -func _000014_add_not_null_constraintUpSqlBytes() ([]byte, error) { - return bindataRead( - __000014_add_not_null_constraintUpSql, - "000014_add_not_null_constraint.up.sql", - ) -} - -func _000014_add_not_null_constraintUpSql() (*asset, error) { - bytes, err := _000014_add_not_null_constraintUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000014_add_not_null_constraint.up.sql", size: 483, mode: os.FileMode(436), modTime: time.Unix(1635256817, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000015_blocks_history_no_nullsDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\xcc\x31\x6e\x85\x30\x10\x84\xe1\xde\xa7\x18\xd1\x1b\x29\x75\xca\x88\x2e\x4d\x94\x48\xa9\x2d\x18\xe2\x95\xcc\x3a\xd9\x35\x41\xdc\xfe\x09\x78\x05\xf5\xcc\xf7\xc7\x08\xad\x30\xfe\xd3\x9c\x58\xe4\xc7\x52\x93\xaa\x98\xab\xa1\x65\x71\x1c\x83\x54\x0d\x21\x46\x7c\x13\x4a\x4e\x48\xf8\x5b\x69\x3b\x32\x8d\xa8\x2d\xd3\x36\x71\xa2\xe5\x7b\x61\x93\x52\x60\xf4\xb5\xb4\xc3\x8a\x22\x29\xb8\xfc\xb6\xfd\xa9\xb7\x4c\x3d\x8d\xcc\x18\xab\x4e\x72\x32\x71\xcc\xa9\x38\xfb\x03\x0d\xb7\xfb\x98\x56\xa7\x23\xa1\xfb\xb8\x78\xf2\x2b\xd7\x81\x66\xd5\xfa\xf0\x39\xbc\x0f\x6f\x5f\x78\x79\x0d\x8f\x00\x00\x00\xff\xff\x45\x94\x91\x43\xd6\x00\x00\x00") - -func _000015_blocks_history_no_nullsDownSqlBytes() ([]byte, error) { - return bindataRead( - __000015_blocks_history_no_nullsDownSql, - "000015_blocks_history_no_nulls.down.sql", - ) -} - -func _000015_blocks_history_no_nullsDownSql() (*asset, error) { - bytes, err := _000015_blocks_history_no_nullsDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000015_blocks_history_no_nulls.down.sql", size: 214, mode: os.FileMode(436), modTime: time.Unix(1641495576, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000015_blocks_history_no_nullsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe4\x96\xcd\x8e\x9b\x30\x10\xc7\xcf\xf6\x53\xf8\xc6\x26\xaa\x92\xb2\xc7\xad\x38\xd0\xc4\x55\x2b\xa5\x9b\x2a\x64\x55\xf5\x14\x25\xc1\x69\xdc\x92\x40\xb1\x2b\x15\x21\xde\xbd\xb2\x31\x18\x93\xcd\x07\x0e\xb7\xbd\x0e\x9e\xbf\x3d\x3f\xe6\x2b\xcf\xe9\x0e\x8d\x0e\x19\xfb\x13\x15\x05\x84\x2f\xdf\xa6\xfe\x12\xa3\x3c\x1f\x25\x29\xd9\xd1\x7f\x45\xb1\x89\xe2\xed\x6f\xb6\xda\x53\xc6\xe3\x34\x43\x7e\x80\x36\x7b\x14\xe0\x25\xda\xec\x47\xc9\x3a\x25\x47\xbe\xa2\xa1\xe7\x38\xe8\xfb\x67\xbc\xc0\x86\x15\x7d\x09\xd0\xf3\xcb\x6c\xf6\xa1\xa3\x2c\xdb\xee\xc9\x61\xed\xb9\x5a\xb2\xb4\xd8\xea\xf1\x2c\x21\xc6\x0b\x85\xc1\x5a\x8c\xf2\xa8\xa5\x26\x2c\xb6\x72\x3b\x4a\xa2\x90\x19\x7a\xa5\xc9\x56\x70\x9b\x92\x35\x27\xab\x35\xf7\xde\x6b\xc9\xda\x68\xab\x9a\xc6\xf1\xc9\x7f\x56\xb6\xfb\x1e\x1a\xae\x36\x99\xe7\xb0\x8c\x71\x72\x70\xda\x0f\x16\x1f\xb5\x3c\xcc\x73\x12\x31\x22\xd2\x74\x3c\x44\x3a\xcb\x86\xe3\x1b\x6f\x76\x21\x10\x77\x6b\x4f\x0f\x4d\xe6\xfe\x0c\x07\x13\xfc\x00\x01\x78\x08\xf0\x0c\x4f\xc4\xdb\x1e\x1b\x39\x0c\x01\xf8\xb4\x98\x7f\xbd\xa6\xfd\x08\x01\xa8\x5e\xef\x8e\xa4\xb6\xd0\xa1\x21\xf2\x9f\xa7\x2d\x49\x11\xd1\x7c\x29\xa3\x12\xf2\xf3\xc5\x14\x2f\xd0\xc7\x1f\xa5\xc3\x91\x91\x94\x8b\x3f\xe5\x07\x13\x14\xd1\x03\xe5\xc8\x1d\x20\x08\xde\x21\xc7\x19\xc0\xf2\x8a\x57\x2a\x4c\x20\x51\x55\xd2\x95\x87\x72\x3b\x0f\x43\x1d\xe8\x8d\x44\xa3\x9a\x2d\x30\xb8\x15\x85\x76\x53\x10\x08\x64\x65\x77\x05\x20\x9d\xce\x87\x2f\x3f\xf7\x16\x7c\xdd\x7b\xee\xca\x00\xb3\x83\xc9\xc8\x65\x17\xea\x1c\xba\xf4\xba\x10\xbb\xfc\xde\x5f\xf0\x75\xab\xbc\x2f\x7a\xb3\xe3\x8a\xf0\x55\xd3\x1c\x8e\x61\x39\xd1\x92\x98\xf1\x9f\x29\x61\x45\x01\xc1\xcd\x44\x24\x12\xa5\x64\x32\x31\xa0\xa8\x13\xc2\x7c\x2b\x96\x2b\x5c\x1a\x2d\xbf\x01\xe6\x46\x32\x02\x4d\x5e\x38\x4f\x4f\xbf\x58\x7c\x1c\x40\x75\x55\x7b\x8a\xd4\xcd\xf3\x2d\xe0\x70\x2e\x61\x38\x86\x6a\x84\xe8\xa9\xd8\xb5\x6c\xb4\xe7\xf9\xd2\xd1\x67\x7a\x2b\x1f\x73\x8c\x5b\x94\x90\x10\xfe\x9b\x84\xa5\x48\x55\x4d\xaf\x2c\x07\x82\x4e\x35\xde\xbb\xb2\xa9\xfc\xce\x93\xa9\x4e\xf4\xc6\xa5\xb9\x89\xdc\xd5\x58\x4e\x56\x1a\x9d\x26\x72\x17\xb1\xcb\x13\xe9\x7a\x2d\x51\xe4\xa1\x9e\x33\xa5\xde\x9f\x6c\xa0\xa8\x9d\xcc\xcc\x92\xd3\x8d\x4c\x56\xd3\xff\x00\x00\x00\xff\xff\x18\x8a\x12\x4b\x46\x0c\x00\x00") - -func _000015_blocks_history_no_nullsUpSqlBytes() ([]byte, error) { - return bindataRead( - __000015_blocks_history_no_nullsUpSql, - "000015_blocks_history_no_nulls.up.sql", - ) -} - -func _000015_blocks_history_no_nullsUpSql() (*asset, error) { - bytes, err := _000015_blocks_history_no_nullsUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000015_blocks_history_no_nulls.up.sql", size: 3142, mode: os.FileMode(436), modTime: time.Unix(1642530526, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000016_subscriptions_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xa8\xae\xd6\x2b\x28\x4a\x4d\xcb\xac\xa8\xad\x2d\x2e\x4d\x2a\x4e\x2e\xca\x2c\x28\xc9\xcc\xcf\x2b\xb6\xe6\xc2\xae\x28\x2f\xbf\x24\x33\x2d\x33\x39\x11\xa4\x28\x3e\x23\x33\xaf\xa4\xd8\x9a\x0b\x10\x00\x00\xff\xff\x8c\x8f\xec\x6f\x4f\x00\x00\x00") - -func _000016_subscriptions_tableDownSqlBytes() ([]byte, error) { - return bindataRead( - __000016_subscriptions_tableDownSql, - "000016_subscriptions_table.down.sql", - ) -} - -func _000016_subscriptions_tableDownSql() (*asset, error) { - bytes, err := _000016_subscriptions_tableDownSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000016_subscriptions_table.down.sql", size: 79, mode: os.FileMode(436), modTime: time.Unix(1641495576, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var __000016_subscriptions_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x90\x5f\x4b\xf3\x30\x14\x87\xaf\xd7\x4f\x71\x2e\x5b\x28\xe3\x7d\x51\x44\xf0\x2a\xab\x99\x16\xe7\x94\x34\x8a\xbb\x2a\xfd\x73\x8a\x61\x6d\x53\x93\x0c\x2d\x21\xdf\x5d\xea\x9c\x38\x3b\x50\xc4\xdb\xdf\x43\x4e\x1e\x9e\x88\x51\xc2\x29\x70\x32\x5b\x50\x88\xe7\xb0\xbc\xe1\x40\x1f\xe2\x84\x27\x60\xed\xb4\x53\x58\x89\x17\xe7\xf4\x26\xd7\x85\x12\x9d\x11\xb2\xd5\xe0\x7b\x93\xbc\x96\xc5\x3a\x35\x7d\x87\x70\x4f\x58\x74\x49\x98\xff\xff\x5f\x10\xee\x80\x28\x3f\xe6\xa3\x93\x61\x7e\x96\x6a\xad\xbb\xac\xc0\x31\x7a\xbf\x9d\xa3\x3a\x74\xef\x13\x1d\xbd\x6c\xa5\x11\x95\xc0\x32\xcd\x0c\xcc\xe2\x8b\x78\xc9\x43\x6f\x52\x28\xcc\x0c\xee\x4d\x25\xd6\xf8\x65\xba\x65\xf1\x35\x61\x2b\xb8\xa2\x2b\xf0\x77\xce\x21\xec\xfd\x16\x78\x01\x58\x2b\x2a\x98\x36\xbd\x7e\xaa\x9d\x3b\xa7\x73\x72\xb7\xe0\x30\x28\x90\x88\x53\x06\x09\xe5\xb0\x31\xd5\x69\x93\x1f\x5b\x8b\x6d\xe9\xdc\x99\xe7\xfd\x2c\xe9\x56\xbe\xc8\x86\xa4\xe9\xa3\x68\xcd\x5f\x77\x6d\x64\xb9\xad\x93\xf7\x63\x78\xa0\xd2\x9b\x50\xff\x7d\xa5\xdf\x66\x79\x0d\x00\x00\xff\xff\xbe\x9d\xee\xc3\x6a\x02\x00\x00") - -func _000016_subscriptions_tableUpSqlBytes() ([]byte, error) { - return bindataRead( - __000016_subscriptions_tableUpSql, - "000016_subscriptions_table.up.sql", - ) -} - -func _000016_subscriptions_tableUpSql() (*asset, error) { - bytes, err := _000016_subscriptions_tableUpSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "000016_subscriptions_table.up.sql", size: 618, mode: os.FileMode(436), modTime: time.Unix(1641495576, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func Asset(name string) ([]byte, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, fmt.Errorf("Asset %s not found", name) -} - -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -func MustAsset(name string) []byte { - a, err := Asset(name) - if err != nil { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func AssetInfo(name string) (os.FileInfo, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, fmt.Errorf("AssetInfo %s not found", name) -} - -// AssetNames returns the names of the assets. -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string]func() (*asset, error){ - "000001_init.down.sql": _000001_initDownSql, - "000001_init.up.sql": _000001_initUpSql, - "000002_system_settings_table.down.sql": _000002_system_settings_tableDownSql, - "000002_system_settings_table.up.sql": _000002_system_settings_tableUpSql, - "000003_blocks_rootid.down.sql": _000003_blocks_rootidDownSql, - "000003_blocks_rootid.up.sql": _000003_blocks_rootidUpSql, - "000004_auth_table.down.sql": _000004_auth_tableDownSql, - "000004_auth_table.up.sql": _000004_auth_tableUpSql, - "000005_blocks_modifiedby.down.sql": _000005_blocks_modifiedbyDownSql, - "000005_blocks_modifiedby.up.sql": _000005_blocks_modifiedbyUpSql, - "000006_sharing_table.down.sql": _000006_sharing_tableDownSql, - "000006_sharing_table.up.sql": _000006_sharing_tableUpSql, - "000007_workspaces_table.down.sql": _000007_workspaces_tableDownSql, - "000007_workspaces_table.up.sql": _000007_workspaces_tableUpSql, - "000008_teams.down.sql": _000008_teamsDownSql, - "000008_teams.up.sql": _000008_teamsUpSql, - "000009_blocks_history.down.sql": _000009_blocks_historyDownSql, - "000009_blocks_history.up.sql": _000009_blocks_historyUpSql, - "000010_blocks_created_by.down.sql": _000010_blocks_created_byDownSql, - "000010_blocks_created_by.up.sql": _000010_blocks_created_byUpSql, - "000011_match_collation.down.sql": _000011_match_collationDownSql, - "000011_match_collation.up.sql": _000011_match_collationUpSql, - "000012_match_column_collation.down.sql": _000012_match_column_collationDownSql, - "000012_match_column_collation.up.sql": _000012_match_column_collationUpSql, - "000013_millisecond_timestamps.down.sql": _000013_millisecond_timestampsDownSql, - "000013_millisecond_timestamps.up.sql": _000013_millisecond_timestampsUpSql, - "000014_add_not_null_constraint.down.sql": _000014_add_not_null_constraintDownSql, - "000014_add_not_null_constraint.up.sql": _000014_add_not_null_constraintUpSql, - "000015_blocks_history_no_nulls.down.sql": _000015_blocks_history_no_nullsDownSql, - "000015_blocks_history_no_nulls.up.sql": _000015_blocks_history_no_nullsUpSql, - "000016_subscriptions_table.down.sql": _000016_subscriptions_tableDownSql, - "000016_subscriptions_table.up.sql": _000016_subscriptions_tableUpSql, -} - -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"} -// AssetDir("data/img") would return []string{"a.png", "b.png"} -// AssetDir("foo.txt") and AssetDir("notexist") would return an error -// AssetDir("") will return []string{"data"}. -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - cannonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(cannonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - } - } - if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} -var _bintree = &bintree{nil, map[string]*bintree{ - "000001_init.down.sql": &bintree{_000001_initDownSql, map[string]*bintree{}}, - "000001_init.up.sql": &bintree{_000001_initUpSql, map[string]*bintree{}}, - "000002_system_settings_table.down.sql": &bintree{_000002_system_settings_tableDownSql, map[string]*bintree{}}, - "000002_system_settings_table.up.sql": &bintree{_000002_system_settings_tableUpSql, map[string]*bintree{}}, - "000003_blocks_rootid.down.sql": &bintree{_000003_blocks_rootidDownSql, map[string]*bintree{}}, - "000003_blocks_rootid.up.sql": &bintree{_000003_blocks_rootidUpSql, map[string]*bintree{}}, - "000004_auth_table.down.sql": &bintree{_000004_auth_tableDownSql, map[string]*bintree{}}, - "000004_auth_table.up.sql": &bintree{_000004_auth_tableUpSql, map[string]*bintree{}}, - "000005_blocks_modifiedby.down.sql": &bintree{_000005_blocks_modifiedbyDownSql, map[string]*bintree{}}, - "000005_blocks_modifiedby.up.sql": &bintree{_000005_blocks_modifiedbyUpSql, map[string]*bintree{}}, - "000006_sharing_table.down.sql": &bintree{_000006_sharing_tableDownSql, map[string]*bintree{}}, - "000006_sharing_table.up.sql": &bintree{_000006_sharing_tableUpSql, map[string]*bintree{}}, - "000007_workspaces_table.down.sql": &bintree{_000007_workspaces_tableDownSql, map[string]*bintree{}}, - "000007_workspaces_table.up.sql": &bintree{_000007_workspaces_tableUpSql, map[string]*bintree{}}, - "000008_teams.down.sql": &bintree{_000008_teamsDownSql, map[string]*bintree{}}, - "000008_teams.up.sql": &bintree{_000008_teamsUpSql, map[string]*bintree{}}, - "000009_blocks_history.down.sql": &bintree{_000009_blocks_historyDownSql, map[string]*bintree{}}, - "000009_blocks_history.up.sql": &bintree{_000009_blocks_historyUpSql, map[string]*bintree{}}, - "000010_blocks_created_by.down.sql": &bintree{_000010_blocks_created_byDownSql, map[string]*bintree{}}, - "000010_blocks_created_by.up.sql": &bintree{_000010_blocks_created_byUpSql, map[string]*bintree{}}, - "000011_match_collation.down.sql": &bintree{_000011_match_collationDownSql, map[string]*bintree{}}, - "000011_match_collation.up.sql": &bintree{_000011_match_collationUpSql, map[string]*bintree{}}, - "000012_match_column_collation.down.sql": &bintree{_000012_match_column_collationDownSql, map[string]*bintree{}}, - "000012_match_column_collation.up.sql": &bintree{_000012_match_column_collationUpSql, map[string]*bintree{}}, - "000013_millisecond_timestamps.down.sql": &bintree{_000013_millisecond_timestampsDownSql, map[string]*bintree{}}, - "000013_millisecond_timestamps.up.sql": &bintree{_000013_millisecond_timestampsUpSql, map[string]*bintree{}}, - "000014_add_not_null_constraint.down.sql": &bintree{_000014_add_not_null_constraintDownSql, map[string]*bintree{}}, - "000014_add_not_null_constraint.up.sql": &bintree{_000014_add_not_null_constraintUpSql, map[string]*bintree{}}, - "000015_blocks_history_no_nulls.down.sql": &bintree{_000015_blocks_history_no_nullsDownSql, map[string]*bintree{}}, - "000015_blocks_history_no_nulls.up.sql": &bintree{_000015_blocks_history_no_nullsUpSql, map[string]*bintree{}}, - "000016_subscriptions_table.down.sql": &bintree{_000016_subscriptions_tableDownSql, map[string]*bintree{}}, - "000016_subscriptions_table.up.sql": &bintree{_000016_subscriptions_tableUpSql, map[string]*bintree{}}, -}} - -// RestoreAsset restores an asset under the given directory -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) - if err != nil { - return err - } - return nil -} - -// RestoreAssets restores an asset under the given directory recursively -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - cannonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) -} - diff --git a/server/services/store/sqlstore/migrations/migrations.go b/server/services/store/sqlstore/migrations/migrations.go deleted file mode 100644 index 7642fd870..000000000 --- a/server/services/store/sqlstore/migrations/migrations.go +++ /dev/null @@ -1,2 +0,0 @@ -//go:generate go-bindata -prefix migrations_files/ -pkg migrations -o bindata.go ./migrations_files -package migrations diff --git a/server/services/store/sqlstore/migrations/postgres_files/000009_blocks_history.down.sql b/server/services/store/sqlstore/migrations/postgres_files/000009_blocks_history.down.sql deleted file mode 100644 index aec3f78e0..000000000 --- a/server/services/store/sqlstore/migrations/postgres_files/000009_blocks_history.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE {{.prefix}}blocks; -ALTER TABLE {{.prefix}}blocks_history RENAME TO {{.prefix}}blocks; diff --git a/server/services/store/sqlstore/migrations/sqlite_files/000009_blocks_history.down.sql b/server/services/store/sqlstore/migrations/sqlite_files/000009_blocks_history.down.sql deleted file mode 100644 index aec3f78e0..000000000 --- a/server/services/store/sqlstore/migrations/sqlite_files/000009_blocks_history.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE {{.prefix}}blocks; -ALTER TABLE {{.prefix}}blocks_history RENAME TO {{.prefix}}blocks; diff --git a/server/services/store/sqlstore/notificationhints.go b/server/services/store/sqlstore/notificationhints.go index 700cff8de..b617614f2 100644 --- a/server/services/store/sqlstore/notificationhints.go +++ b/server/services/store/sqlstore/notificationhints.go @@ -19,7 +19,6 @@ import ( var notificationHintFields = []string{ "block_type", "block_id", - "workspace_id", "modified_by_id", "create_at", "notify_at", @@ -29,7 +28,6 @@ func valuesForNotificationHint(hint *model.NotificationHint) []interface{} { return []interface{}{ hint.BlockType, hint.BlockID, - hint.WorkspaceID, hint.ModifiedByID, hint.CreateAt, hint.NotifyAt, @@ -44,7 +42,6 @@ func (s *SQLStore) notificationHintFromRows(rows *sql.Rows) ([]*model.Notificati err := rows.Scan( &hint.BlockType, &hint.BlockID, - &hint.WorkspaceID, &hint.ModifiedByID, &hint.CreateAt, &hint.NotifyAt, @@ -73,7 +70,7 @@ func (s *SQLStore) upsertNotificationHint(db sq.BaseRunner, hint *model.Notifica Columns(notificationHintFields...). Values(valuesForNotificationHint(hint)...) - if s.dbType == mysqlDBType { + if s.dbType == model.MysqlDBType { query = query.Suffix("ON DUPLICATE KEY UPDATE notify_at = ?", notifyAt) } else { query = query.Suffix("ON CONFLICT (block_id) DO UPDATE SET notify_at = ?", notifyAt) @@ -82,7 +79,6 @@ func (s *SQLStore) upsertNotificationHint(db sq.BaseRunner, hint *model.Notifica if _, err := query.Exec(); err != nil { s.logger.Error("Cannot upsert notification hint", mlog.String("block_id", hint.BlockID), - mlog.String("workspace_id", hint.WorkspaceID), mlog.Err(err), ) return nil, err @@ -91,11 +87,10 @@ func (s *SQLStore) upsertNotificationHint(db sq.BaseRunner, hint *model.Notifica } // deleteNotificationHint deletes the notification hint for the specified block. -func (s *SQLStore) deleteNotificationHint(db sq.BaseRunner, c store.Container, blockID string) error { +func (s *SQLStore) deleteNotificationHint(db sq.BaseRunner, blockID string) error { query := s.getQueryBuilder(db). Delete(s.tablePrefix + "notification_hints"). - Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}) + Where(sq.Eq{"block_id": blockID}) result, err := query.Exec() if err != nil { @@ -115,18 +110,16 @@ func (s *SQLStore) deleteNotificationHint(db sq.BaseRunner, c store.Container, b } // getNotificationHint fetches the notification hint for the specified block. -func (s *SQLStore) getNotificationHint(db sq.BaseRunner, c store.Container, blockID string) (*model.NotificationHint, error) { +func (s *SQLStore) getNotificationHint(db sq.BaseRunner, blockID string) (*model.NotificationHint, error) { query := s.getQueryBuilder(db). Select(notificationHintFields...). From(s.tablePrefix + "notification_hints"). - Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}) + Where(sq.Eq{"block_id": blockID}) rows, err := query.Query() if err != nil { s.logger.Error("Cannot fetch notification hint", mlog.String("block_id", blockID), - mlog.String("workspace_id", c.WorkspaceID), mlog.Err(err), ) return nil, err @@ -137,7 +130,6 @@ func (s *SQLStore) getNotificationHint(db sq.BaseRunner, c store.Container, bloc if err != nil { s.logger.Error("Cannot get notification hint", mlog.String("block_id", blockID), - mlog.String("workspace_id", c.WorkspaceID), mlog.Err(err), ) return nil, err diff --git a/server/services/store/sqlstore/public_methods.go b/server/services/store/sqlstore/public_methods.go index b4db35ba9..8861bbbf3 100644 --- a/server/services/store/sqlstore/public_methods.go +++ b/server/services/store/sqlstore/public_methods.go @@ -17,18 +17,70 @@ import ( "time" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) +func (s *SQLStore) AddUpdateCategoryBlock(userID string, categoryID string, blockID string) error { + return s.addUpdateCategoryBlock(s.db, userID, categoryID, blockID) + +} + func (s *SQLStore) CleanUpSessions(expireTime int64) error { return s.cleanUpSessions(s.db, expireTime) } -func (s *SQLStore) CreatePrivateWorkspace(userID string) (string, error) { - return s.createPrivateWorkspace(s.db, userID) +func (s *SQLStore) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { + if s.dbType == model.SqliteDBType { + return s.createBoardsAndBlocks(s.db, bab, userID) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return nil, txErr + } + result, err := s.createBoardsAndBlocks(tx, bab, userID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateBoardsAndBlocks")) + } + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return result, nil + +} + +func (s *SQLStore) CreateBoardsAndBlocksWithAdmin(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) { + if s.dbType == model.SqliteDBType { + return s.createBoardsAndBlocksWithAdmin(s.db, bab, userID) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return nil, nil, txErr + } + result, resultVar1, err := s.createBoardsAndBlocksWithAdmin(tx, bab, userID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateBoardsAndBlocksWithAdmin")) + } + return nil, nil, err + } + + if err := tx.Commit(); err != nil { + return nil, nil, err + } + + return result, resultVar1, nil + +} + +func (s *SQLStore) CreateCategory(category model.Category) error { + return s.createCategory(s.db, category) } @@ -37,8 +89,8 @@ func (s *SQLStore) CreateSession(session *model.Session) error { } -func (s *SQLStore) CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error) { - return s.createSubscription(s.db, c, sub) +func (s *SQLStore) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) { + return s.createSubscription(s.db, sub) } @@ -47,15 +99,15 @@ func (s *SQLStore) CreateUser(user *model.User) error { } -func (s *SQLStore) DeleteBlock(c store.Container, blockID string, modifiedBy string) error { - if s.dbType == sqliteDBType { - return s.deleteBlock(s.db, c, blockID, modifiedBy) +func (s *SQLStore) DeleteBlock(blockID string, modifiedBy string) error { + if s.dbType == model.SqliteDBType { + return s.deleteBlock(s.db, blockID, modifiedBy) } tx, txErr := s.db.BeginTx(context.Background(), nil) if txErr != nil { return txErr } - err := s.deleteBlock(tx, c, blockID, modifiedBy) + err := s.deleteBlock(tx, blockID, modifiedBy) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBlock")) @@ -71,8 +123,66 @@ func (s *SQLStore) DeleteBlock(c store.Container, blockID string, modifiedBy str } -func (s *SQLStore) DeleteNotificationHint(c store.Container, blockID string) error { - return s.deleteNotificationHint(s.db, c, blockID) +func (s *SQLStore) DeleteBoard(boardID string, userID string) error { + if s.dbType == model.SqliteDBType { + return s.deleteBoard(s.db, boardID, userID) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return txErr + } + err := s.deleteBoard(tx, boardID, userID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBoard")) + } + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil + +} + +func (s *SQLStore) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error { + if s.dbType == model.SqliteDBType { + return s.deleteBoardsAndBlocks(s.db, dbab, userID) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return txErr + } + err := s.deleteBoardsAndBlocks(tx, dbab, userID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBoardsAndBlocks")) + } + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil + +} + +func (s *SQLStore) DeleteCategory(categoryID string, userID string, teamID string) error { + return s.deleteCategory(s.db, categoryID, userID, teamID) + +} + +func (s *SQLStore) DeleteMember(boardID string, userID string) error { + return s.deleteMember(s.db, boardID, userID) + +} + +func (s *SQLStore) DeleteNotificationHint(blockID string) error { + return s.deleteNotificationHint(s.db, blockID) } @@ -81,8 +191,56 @@ func (s *SQLStore) DeleteSession(sessionID string) error { } -func (s *SQLStore) DeleteSubscription(c store.Container, blockID string, subscriberID string) error { - return s.deleteSubscription(s.db, c, blockID, subscriberID) +func (s *SQLStore) DeleteSubscription(blockID string, subscriberID string) error { + return s.deleteSubscription(s.db, blockID, subscriberID) + +} + +func (s *SQLStore) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) { + if s.dbType == model.SqliteDBType { + return s.duplicateBlock(s.db, boardID, blockID, userID, asTemplate) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return nil, txErr + } + result, err := s.duplicateBlock(tx, boardID, blockID, userID, asTemplate) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DuplicateBlock")) + } + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return result, nil + +} + +func (s *SQLStore) DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { + if s.dbType == model.SqliteDBType { + return s.duplicateBoard(s.db, boardID, userID, toTeam, asTemplate) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return nil, nil, txErr + } + result, resultVar1, err := s.duplicateBoard(tx, boardID, userID, toTeam, asTemplate) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DuplicateBoard")) + } + return nil, nil, err + } + + if err := tx.Commit(); err != nil { + return nil, nil, err + } + + return result, resultVar1, nil } @@ -91,13 +249,13 @@ func (s *SQLStore) GetActiveUserCount(updatedSecondsAgo int64) (int, error) { } -func (s *SQLStore) GetAllBlocks(c store.Container) ([]model.Block, error) { - return s.getAllBlocks(s.db, c) +func (s *SQLStore) GetAllTeams() ([]*model.Team, error) { + return s.getAllTeams(s.db) } -func (s *SQLStore) GetBlock(c store.Container, blockID string) (*model.Block, error) { - return s.getBlock(s.db, c, blockID) +func (s *SQLStore) GetBlock(blockID string) (*model.Block, error) { + return s.getBlock(s.db, blockID) } @@ -106,43 +264,73 @@ func (s *SQLStore) GetBlockCountsByType() (map[string]int64, error) { } -func (s *SQLStore) GetBlockHistory(c store.Container, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) { - return s.getBlockHistory(s.db, c, blockID, opts) +func (s *SQLStore) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) { + return s.getBlockHistory(s.db, blockID, opts) } -func (s *SQLStore) GetBlocksWithParent(c store.Container, parentID string) ([]model.Block, error) { - return s.getBlocksWithParent(s.db, c, parentID) +func (s *SQLStore) GetBlocksForBoard(boardID string) ([]model.Block, error) { + return s.getBlocksForBoard(s.db, boardID) } -func (s *SQLStore) GetBlocksWithParentAndType(c store.Container, parentID string, blockType string) ([]model.Block, error) { - return s.getBlocksWithParentAndType(s.db, c, parentID, blockType) +func (s *SQLStore) GetBlocksWithBoardID(boardID string) ([]model.Block, error) { + return s.getBlocksWithBoardID(s.db, boardID) } -func (s *SQLStore) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) { - return s.getBlocksWithRootID(s.db, c, rootID) +func (s *SQLStore) GetBlocksWithParent(boardID string, parentID string) ([]model.Block, error) { + return s.getBlocksWithParent(s.db, boardID, parentID) } -func (s *SQLStore) GetBlocksWithType(c store.Container, blockType string) ([]model.Block, error) { - return s.getBlocksWithType(s.db, c, blockType) +func (s *SQLStore) GetBlocksWithParentAndType(boardID string, parentID string, blockType string) ([]model.Block, error) { + return s.getBlocksWithParentAndType(s.db, boardID, parentID, blockType) } -func (s *SQLStore) GetBoardAndCard(c store.Container, block *model.Block) (*model.Block, *model.Block, error) { - return s.getBoardAndCard(s.db, c, block) +func (s *SQLStore) GetBlocksWithType(boardID string, blockType string) ([]model.Block, error) { + return s.getBlocksWithType(s.db, boardID, blockType) } -func (s *SQLStore) GetBoardAndCardByID(c store.Container, blockID string) (*model.Block, *model.Block, error) { - return s.getBoardAndCardByID(s.db, c, blockID) +func (s *SQLStore) GetBoard(id string) (*model.Board, error) { + return s.getBoard(s.db, id) } -func (s *SQLStore) GetDefaultTemplateBlocks() ([]model.Block, error) { - return s.getDefaultTemplateBlocks(s.db) +func (s *SQLStore) GetBoardAndCard(block *model.Block) (*model.Board, *model.Block, error) { + return s.getBoardAndCard(s.db, block) + +} + +func (s *SQLStore) GetBoardAndCardByID(blockID string) (*model.Board, *model.Block, error) { + return s.getBoardAndCardByID(s.db, blockID) + +} + +func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*model.Board, error) { + return s.getBoardsForUserAndTeam(s.db, userID, teamID) + +} + +func (s *SQLStore) GetCategory(id string) (*model.Category, error) { + return s.getCategory(s.db, id) + +} + +func (s *SQLStore) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) { + return s.getMemberForBoard(s.db, boardID, userID) + +} + +func (s *SQLStore) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { + return s.getMembersForBoard(s.db, boardID) + +} + +func (s *SQLStore) GetMembersForUser(userID string) ([]*model.BoardMember, error) { + return s.getMembersForUser(s.db, userID) } @@ -151,13 +339,8 @@ func (s *SQLStore) GetNextNotificationHint(remove bool) (*model.NotificationHint } -func (s *SQLStore) GetNotificationHint(c store.Container, blockID string) (*model.NotificationHint, error) { - return s.getNotificationHint(s.db, c, blockID) - -} - -func (s *SQLStore) GetParentID(c store.Container, blockID string) (string, error) { - return s.getParentID(s.db, c, blockID) +func (s *SQLStore) GetNotificationHint(blockID string) (*model.NotificationHint, error) { + return s.getNotificationHint(s.db, blockID) } @@ -166,48 +349,43 @@ func (s *SQLStore) GetRegisteredUserCount() (int, error) { } -func (s *SQLStore) GetRootID(c store.Container, blockID string) (string, error) { - return s.getRootID(s.db, c, blockID) - -} - func (s *SQLStore) GetSession(token string, expireTime int64) (*model.Session, error) { return s.getSession(s.db, token, expireTime) } -func (s *SQLStore) GetSharing(c store.Container, rootID string) (*model.Sharing, error) { - return s.getSharing(s.db, c, rootID) +func (s *SQLStore) GetSharing(rootID string) (*model.Sharing, error) { + return s.getSharing(s.db, rootID) } -func (s *SQLStore) GetSubTree2(c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { - return s.getSubTree2(s.db, c, blockID, opts) +func (s *SQLStore) GetSubTree2(boardID string, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { + return s.getSubTree2(s.db, boardID, blockID, opts) } -func (s *SQLStore) GetSubTree3(c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { - return s.getSubTree3(s.db, c, blockID, opts) +func (s *SQLStore) GetSubTree3(boardID string, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) { + return s.getSubTree3(s.db, boardID, blockID, opts) } -func (s *SQLStore) GetSubscribersCountForBlock(c store.Container, blockID string) (int, error) { - return s.getSubscribersCountForBlock(s.db, c, blockID) +func (s *SQLStore) GetSubscribersCountForBlock(blockID string) (int, error) { + return s.getSubscribersCountForBlock(s.db, blockID) } -func (s *SQLStore) GetSubscribersForBlock(c store.Container, blockID string) ([]*model.Subscriber, error) { - return s.getSubscribersForBlock(s.db, c, blockID) +func (s *SQLStore) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) { + return s.getSubscribersForBlock(s.db, blockID) } -func (s *SQLStore) GetSubscription(c store.Container, blockID string, subscriberID string) (*model.Subscription, error) { - return s.getSubscription(s.db, c, blockID, subscriberID) +func (s *SQLStore) GetSubscription(blockID string, subscriberID string) (*model.Subscription, error) { + return s.getSubscription(s.db, blockID, subscriberID) } -func (s *SQLStore) GetSubscriptions(c store.Container, subscriberID string) ([]*model.Subscription, error) { - return s.getSubscriptions(s.db, c, subscriberID) +func (s *SQLStore) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) { + return s.getSubscriptions(s.db, subscriberID) } @@ -221,6 +399,26 @@ func (s *SQLStore) GetSystemSettings() (map[string]string, error) { } +func (s *SQLStore) GetTeam(ID string) (*model.Team, error) { + return s.getTeam(s.db, ID) + +} + +func (s *SQLStore) GetTeamCount() (int64, error) { + return s.getTeamCount(s.db) + +} + +func (s *SQLStore) GetTeamsForUser(userID string) ([]*model.Team, error) { + return s.getTeamsForUser(s.db, userID) + +} + +func (s *SQLStore) GetTemplateBoards(teamID string) ([]*model.Board, error) { + return s.getTemplateBoards(s.db, teamID) + +} + func (s *SQLStore) GetUserByEmail(email string) (*model.User, error) { return s.getUserByEmail(s.db, email) @@ -236,40 +434,25 @@ func (s *SQLStore) GetUserByUsername(username string) (*model.User, error) { } -func (s *SQLStore) GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) { - return s.getUserWorkspaces(s.db, userID) +func (s *SQLStore) GetUserCategoryBlocks(userID string, teamID string) ([]model.CategoryBlocks, error) { + return s.getUserCategoryBlocks(s.db, userID, teamID) } -func (s *SQLStore) GetUsersByWorkspace(workspaceID string) ([]*model.User, error) { - return s.getUsersByWorkspace(s.db, workspaceID) +func (s *SQLStore) GetUsersByTeam(teamID string) ([]*model.User, error) { + return s.getUsersByTeam(s.db, teamID) } -func (s *SQLStore) GetWorkspace(ID string) (*model.Workspace, error) { - return s.getWorkspace(s.db, ID) - -} - -func (s *SQLStore) GetWorkspaceCount() (int64, error) { - return s.getWorkspaceCount(s.db) - -} - -func (s *SQLStore) HasWorkspaceAccess(userID string, workspaceID string) (bool, error) { - return s.hasWorkspaceAccess(s.db, userID, workspaceID) - -} - -func (s *SQLStore) InsertBlock(c store.Container, block *model.Block, userID string) error { - if s.dbType == sqliteDBType { - return s.insertBlock(s.db, c, block, userID) +func (s *SQLStore) InsertBlock(block *model.Block, userID string) error { + if s.dbType == model.SqliteDBType { + return s.insertBlock(s.db, block, userID) } tx, txErr := s.db.BeginTx(context.Background(), nil) if txErr != nil { return txErr } - err := s.insertBlock(tx, c, block, userID) + err := s.insertBlock(tx, block, userID) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBlock")) @@ -285,15 +468,15 @@ func (s *SQLStore) InsertBlock(c store.Container, block *model.Block, userID str } -func (s *SQLStore) InsertBlocks(c store.Container, blocks []model.Block, userID string) error { - if s.dbType == sqliteDBType { - return s.insertBlocks(s.db, c, blocks, userID) +func (s *SQLStore) InsertBlocks(blocks []model.Block, userID string) error { + if s.dbType == model.SqliteDBType { + return s.insertBlocks(s.db, blocks, userID) } tx, txErr := s.db.BeginTx(context.Background(), nil) if txErr != nil { return txErr } - err := s.insertBlocks(tx, c, blocks, userID) + err := s.insertBlocks(tx, blocks, userID) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBlocks")) @@ -309,15 +492,44 @@ func (s *SQLStore) InsertBlocks(c store.Container, blocks []model.Block, userID } -func (s *SQLStore) PatchBlock(c store.Container, blockID string, blockPatch *model.BlockPatch, userID string) error { - if s.dbType == sqliteDBType { - return s.patchBlock(s.db, c, blockID, blockPatch, userID) +func (s *SQLStore) InsertBoard(board *model.Board, userID string) (*model.Board, error) { + return s.insertBoard(s.db, board, userID) + +} + +func (s *SQLStore) InsertBoardWithAdmin(board *model.Board, userID string) (*model.Board, *model.BoardMember, error) { + if s.dbType == model.SqliteDBType { + return s.insertBoardWithAdmin(s.db, board, userID) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return nil, nil, txErr + } + result, resultVar1, err := s.insertBoardWithAdmin(tx, board, userID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBoardWithAdmin")) + } + return nil, nil, err + } + + if err := tx.Commit(); err != nil { + return nil, nil, err + } + + return result, resultVar1, nil + +} + +func (s *SQLStore) PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error { + if s.dbType == model.SqliteDBType { + return s.patchBlock(s.db, blockID, blockPatch, userID) } tx, txErr := s.db.BeginTx(context.Background(), nil) if txErr != nil { return txErr } - err := s.patchBlock(tx, c, blockID, blockPatch, userID) + err := s.patchBlock(tx, blockID, blockPatch, userID) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBlock")) @@ -333,15 +545,15 @@ func (s *SQLStore) PatchBlock(c store.Container, blockID string, blockPatch *mod } -func (s *SQLStore) PatchBlocks(c store.Container, blockPatches *model.BlockPatchBatch, userID string) error { - if s.dbType == sqliteDBType { - return s.patchBlocks(s.db, c, blockPatches, userID) +func (s *SQLStore) PatchBlocks(blockPatches *model.BlockPatchBatch, userID string) error { + if s.dbType == model.SqliteDBType { + return s.patchBlocks(s.db, blockPatches, userID) } tx, txErr := s.db.BeginTx(context.Background(), nil) if txErr != nil { return txErr } - err := s.patchBlocks(tx, c, blockPatches, userID) + err := s.patchBlocks(tx, blockPatches, userID) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBlocks")) @@ -357,6 +569,54 @@ func (s *SQLStore) PatchBlocks(c store.Container, blockPatches *model.BlockPatch } +func (s *SQLStore) PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) { + if s.dbType == model.SqliteDBType { + return s.patchBoard(s.db, boardID, boardPatch, userID) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return nil, txErr + } + result, err := s.patchBoard(tx, boardID, boardPatch, userID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBoard")) + } + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return result, nil + +} + +func (s *SQLStore) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { + if s.dbType == model.SqliteDBType { + return s.patchBoardsAndBlocks(s.db, pbab, userID) + } + tx, txErr := s.db.BeginTx(context.Background(), nil) + if txErr != nil { + return nil, txErr + } + result, err := s.patchBoardsAndBlocks(tx, pbab, userID) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBoardsAndBlocks")) + } + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return result, nil + +} + func (s *SQLStore) PatchUserProps(userID string, patch model.UserPropPatch) error { return s.patchUserProps(s.db, userID, patch) @@ -367,8 +627,23 @@ func (s *SQLStore) RefreshSession(session *model.Session) error { } -func (s *SQLStore) RemoveDefaultTemplates(blocks []model.Block) error { - return s.removeDefaultTemplates(s.db, blocks) +func (s *SQLStore) RemoveDefaultTemplates(boards []*model.Board) error { + return s.removeDefaultTemplates(s.db, boards) + +} + +func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error) { + return s.saveMember(s.db, bm) + +} + +func (s *SQLStore) SearchBoardsForUserAndTeam(term string, userID string, teamID string) ([]*model.Board, error) { + return s.searchBoardsForUserAndTeam(s.db, term, userID, teamID) + +} + +func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) { + return s.searchUsersByTeam(s.db, teamID, searchQuery) } @@ -377,12 +652,15 @@ func (s *SQLStore) SetSystemSetting(key string, value string) error { } -func (s *SQLStore) UndeleteBlock(c store.Container, blockID string, modifiedBy string) error { +func (s *SQLStore) UndeleteBlock(blockID string, modifiedBy string) error { + if s.dbType == model.SqliteDBType { + return s.undeleteBlock(s.db, blockID, modifiedBy) + } tx, txErr := s.db.BeginTx(context.Background(), nil) if txErr != nil { return txErr } - err := s.undeleteBlock(tx, c, blockID, modifiedBy) + err := s.undeleteBlock(tx, blockID, modifiedBy) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "UndeleteBlock")) @@ -398,13 +676,18 @@ func (s *SQLStore) UndeleteBlock(c store.Container, blockID string, modifiedBy s } +func (s *SQLStore) UpdateCategory(category model.Category) error { + return s.updateCategory(s.db, category) + +} + func (s *SQLStore) UpdateSession(session *model.Session) error { return s.updateSession(s.db, session) } -func (s *SQLStore) UpdateSubscribersNotifiedAt(c store.Container, blockID string, notifiedAt int64) error { - return s.updateSubscribersNotifiedAt(s.db, c, blockID, notifiedAt) +func (s *SQLStore) UpdateSubscribersNotifiedAt(blockID string, notifiedAt int64) error { + return s.updateSubscribersNotifiedAt(s.db, blockID, notifiedAt) } @@ -428,17 +711,17 @@ func (s *SQLStore) UpsertNotificationHint(hint *model.NotificationHint, notifica } -func (s *SQLStore) UpsertSharing(c store.Container, sharing model.Sharing) error { - return s.upsertSharing(s.db, c, sharing) +func (s *SQLStore) UpsertSharing(sharing model.Sharing) error { + return s.upsertSharing(s.db, sharing) } -func (s *SQLStore) UpsertWorkspaceSettings(workspace model.Workspace) error { - return s.upsertWorkspaceSettings(s.db, workspace) +func (s *SQLStore) UpsertTeamSettings(team model.Team) error { + return s.upsertTeamSettings(s.db, team) } -func (s *SQLStore) UpsertWorkspaceSignupToken(workspace model.Workspace) error { - return s.upsertWorkspaceSignupToken(s.db, workspace) +func (s *SQLStore) UpsertTeamSignupToken(team model.Team) error { + return s.upsertTeamSignupToken(s.db, team) } diff --git a/server/services/store/sqlstore/sharing.go b/server/services/store/sqlstore/sharing.go index ee47657b7..56efd0c54 100644 --- a/server/services/store/sqlstore/sharing.go +++ b/server/services/store/sqlstore/sharing.go @@ -2,13 +2,12 @@ package sqlstore import ( "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" sq "github.com/Masterminds/squirrel" ) -func (s *SQLStore) upsertSharing(db sq.BaseRunner, _ store.Container, sharing model.Sharing) error { +func (s *SQLStore) upsertSharing(db sq.BaseRunner, sharing model.Sharing) error { now := utils.GetMillis() query := s.getQueryBuilder(db). @@ -27,7 +26,7 @@ func (s *SQLStore) upsertSharing(db sq.BaseRunner, _ store.Container, sharing mo sharing.ModifiedBy, now, ) - if s.dbType == mysqlDBType { + if s.dbType == model.MysqlDBType { query = query.Suffix("ON DUPLICATE KEY UPDATE enabled = ?, token = ?, modified_by = ?, update_at = ?", sharing.Enabled, sharing.Token, sharing.ModifiedBy, now) } else { @@ -41,7 +40,7 @@ func (s *SQLStore) upsertSharing(db sq.BaseRunner, _ store.Container, sharing mo return err } -func (s *SQLStore) getSharing(db sq.BaseRunner, _ store.Container, rootID string) (*model.Sharing, error) { +func (s *SQLStore) getSharing(db sq.BaseRunner, boardID string) (*model.Sharing, error) { query := s.getQueryBuilder(db). Select( "id", @@ -51,7 +50,7 @@ func (s *SQLStore) getSharing(db sq.BaseRunner, _ store.Container, rootID string "update_at", ). From(s.tablePrefix + "sharing"). - Where(sq.Eq{"id": rootID}) + Where(sq.Eq{"id": boardID}) row := query.QueryRow() sharing := model.Sharing{} diff --git a/server/services/store/sqlstore/sqlstore.go b/server/services/store/sqlstore/sqlstore.go index 190028b87..297514683 100644 --- a/server/services/store/sqlstore/sqlstore.go +++ b/server/services/store/sqlstore/sqlstore.go @@ -5,17 +5,12 @@ import ( sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" "github.com/mattermost/mattermost-plugin-api/cluster" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) -const ( - mysqlDBType = "mysql" - sqliteDBType = "sqlite3" - postgresDBType = "postgres" -) - // SQLStore is a SQL database. type SQLStore struct { db *sql.DB @@ -77,7 +72,7 @@ func (s *SQLStore) DBType() string { func (s *SQLStore) getQueryBuilder(db sq.BaseRunner) sq.StatementBuilderType { builder := sq.StatementBuilder - if s.dbType == postgresDBType || s.dbType == sqliteDBType { + if s.dbType == model.PostgresDBType || s.dbType == model.SqliteDBType { builder = builder.PlaceholderFormat(sq.Dollar) } @@ -85,10 +80,10 @@ func (s *SQLStore) getQueryBuilder(db sq.BaseRunner) sq.StatementBuilderType { } func (s *SQLStore) escapeField(fieldName string) string { //nolint:unparam - if s.dbType == mysqlDBType { + if s.dbType == model.MysqlDBType { return "`" + fieldName + "`" } - if s.dbType == postgresDBType || s.dbType == sqliteDBType { + if s.dbType == model.PostgresDBType || s.dbType == model.SqliteDBType { return "\"" + fieldName + "\"" } return fieldName diff --git a/server/services/store/sqlstore/sqlstore_test.go b/server/services/store/sqlstore/sqlstore_test.go index cc6f46169..9567927af 100644 --- a/server/services/store/sqlstore/sqlstore_test.go +++ b/server/services/store/sqlstore/sqlstore_test.go @@ -9,13 +9,15 @@ import ( "github.com/mattermost/focalboard/server/services/store/storetests" ) -func TestBlocksStore(t *testing.T) { +func TestSQLStore(t *testing.T) { t.Run("BlocksStore", func(t *testing.T) { storetests.StoreTestBlocksStore(t, SetupTests) }) t.Run("SharingStore", func(t *testing.T) { storetests.StoreTestSharingStore(t, SetupTests) }) t.Run("SystemStore", func(t *testing.T) { storetests.StoreTestSystemStore(t, SetupTests) }) t.Run("UserStore", func(t *testing.T) { storetests.StoreTestUserStore(t, SetupTests) }) t.Run("SessionStore", func(t *testing.T) { storetests.StoreTestSessionStore(t, SetupTests) }) - t.Run("WorkspaceStore", func(t *testing.T) { storetests.StoreTestWorkspaceStore(t, SetupTests) }) + t.Run("TeamStore", func(t *testing.T) { storetests.StoreTestTeamStore(t, SetupTests) }) + t.Run("BoardStore", func(t *testing.T) { storetests.StoreTestBoardStore(t, SetupTests) }) + t.Run("BoardsAndBlocksStore", func(t *testing.T) { storetests.StoreTestBoardsAndBlocksStore(t, SetupTests) }) t.Run("SubscriptionStore", func(t *testing.T) { storetests.StoreTestSubscriptionsStore(t, SetupTests) }) t.Run("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(t, SetupTests) }) } diff --git a/server/services/store/sqlstore/subscriptions.go b/server/services/store/sqlstore/subscriptions.go index 1c440e9ea..a25439ec6 100644 --- a/server/services/store/sqlstore/subscriptions.go +++ b/server/services/store/sqlstore/subscriptions.go @@ -16,7 +16,6 @@ import ( var subscriptionFields = []string{ "block_type", "block_id", - "workspace_id", "subscriber_type", "subscriber_id", "notified_at", @@ -28,7 +27,6 @@ func valuesForSubscription(sub *model.Subscription) []interface{} { return []interface{}{ sub.BlockType, sub.BlockID, - sub.WorkspaceID, sub.SubscriberType, sub.SubscriberID, sub.NotifiedAt, @@ -45,7 +43,6 @@ func (s *SQLStore) subscriptionsFromRows(rows *sql.Rows) ([]*model.Subscription, err := rows.Scan( &sub.BlockType, &sub.BlockID, - &sub.WorkspaceID, &sub.SubscriberType, &sub.SubscriberID, &sub.NotifiedAt, @@ -62,9 +59,7 @@ func (s *SQLStore) subscriptionsFromRows(rows *sql.Rows) ([]*model.Subscription, // createSubscription creates a new subscription, or returns an existing subscription // for the block & subscriber. -func (s *SQLStore) createSubscription(db sq.BaseRunner, c store.Container, sub *model.Subscription) (*model.Subscription, error) { - sub.WorkspaceID = c.WorkspaceID - +func (s *SQLStore) createSubscription(db sq.BaseRunner, sub *model.Subscription) (*model.Subscription, error) { if err := sub.IsValid(); err != nil { return nil, err } @@ -81,7 +76,7 @@ func (s *SQLStore) createSubscription(db sq.BaseRunner, c store.Container, sub * Columns(subscriptionFields...). Values(valuesForSubscription(&subAdd)...) - if s.dbType == mysqlDBType { + if s.dbType == model.MysqlDBType { query = query.Suffix("ON DUPLICATE KEY UPDATE delete_at = 0, notified_at = ?", now) } else { query = query.Suffix("ON CONFLICT (block_id,subscriber_id) DO UPDATE SET delete_at = 0, notified_at = ?", now) @@ -90,7 +85,6 @@ func (s *SQLStore) createSubscription(db sq.BaseRunner, c store.Container, sub * if _, err := query.Exec(); err != nil { s.logger.Error("Cannot create subscription", mlog.String("block_id", sub.BlockID), - mlog.String("workspace_id", sub.WorkspaceID), mlog.String("subscriber_id", sub.SubscriberID), mlog.Err(err), ) @@ -100,14 +94,13 @@ func (s *SQLStore) createSubscription(db sq.BaseRunner, c store.Container, sub * } // deleteSubscription soft deletes the subscription for a specific block and subscriber. -func (s *SQLStore) deleteSubscription(db sq.BaseRunner, c store.Container, blockID string, subscriberID string) error { +func (s *SQLStore) deleteSubscription(db sq.BaseRunner, blockID string, subscriberID string) error { now := model.GetMillis() query := s.getQueryBuilder(db). Update(s.tablePrefix+"subscriptions"). Set("delete_at", now). Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}). Where(sq.Eq{"subscriber_id": subscriberID}) result, err := query.Exec() @@ -121,19 +114,18 @@ func (s *SQLStore) deleteSubscription(db sq.BaseRunner, c store.Container, block } if count == 0 { - return store.NewErrNotFound(c.WorkspaceID + "," + blockID + "," + subscriberID) + return store.NewErrNotFound(blockID + "," + subscriberID) } return nil } // getSubscription fetches the subscription for a specific block and subscriber. -func (s *SQLStore) getSubscription(db sq.BaseRunner, c store.Container, blockID string, subscriberID string) (*model.Subscription, error) { +func (s *SQLStore) getSubscription(db sq.BaseRunner, blockID string, subscriberID string) (*model.Subscription, error) { query := s.getQueryBuilder(db). Select(subscriptionFields...). From(s.tablePrefix + "subscriptions"). Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}). Where(sq.Eq{"subscriber_id": subscriberID}). Where(sq.Eq{"delete_at": 0}) @@ -141,7 +133,6 @@ func (s *SQLStore) getSubscription(db sq.BaseRunner, c store.Container, blockID if err != nil { s.logger.Error("Cannot fetch subscription for block & subscriber", mlog.String("block_id", blockID), - mlog.String("workspace_id", c.WorkspaceID), mlog.String("subscriber_id", subscriberID), mlog.Err(err), ) @@ -153,25 +144,23 @@ func (s *SQLStore) getSubscription(db sq.BaseRunner, c store.Container, blockID if err != nil { s.logger.Error("Cannot get subscription for block & subscriber", mlog.String("block_id", blockID), - mlog.String("workspace_id", c.WorkspaceID), mlog.String("subscriber_id", subscriberID), mlog.Err(err), ) return nil, err } if len(subscriptions) == 0 { - return nil, store.NewErrNotFound(c.WorkspaceID + "," + blockID + "," + subscriberID) + return nil, store.NewErrNotFound(blockID + "," + subscriberID) } return subscriptions[0], nil } // getSubscriptions fetches all subscriptions for a specific subscriber. -func (s *SQLStore) getSubscriptions(db sq.BaseRunner, c store.Container, subscriberID string) ([]*model.Subscription, error) { +func (s *SQLStore) getSubscriptions(db sq.BaseRunner, subscriberID string) ([]*model.Subscription, error) { query := s.getQueryBuilder(db). Select(subscriptionFields...). From(s.tablePrefix + "subscriptions"). Where(sq.Eq{"subscriber_id": subscriberID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}). Where(sq.Eq{"delete_at": 0}) rows, err := query.Query() @@ -188,7 +177,7 @@ func (s *SQLStore) getSubscriptions(db sq.BaseRunner, c store.Container, subscri } // getSubscribersForBlock fetches all subscribers for a block. -func (s *SQLStore) getSubscribersForBlock(db sq.BaseRunner, c store.Container, blockID string) ([]*model.Subscriber, error) { +func (s *SQLStore) getSubscribersForBlock(db sq.BaseRunner, blockID string) ([]*model.Subscriber, error) { query := s.getQueryBuilder(db). Select( "subscriber_type", @@ -197,7 +186,6 @@ func (s *SQLStore) getSubscribersForBlock(db sq.BaseRunner, c store.Container, b ). From(s.tablePrefix + "subscriptions"). Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}). Where(sq.Eq{"delete_at": 0}). OrderBy("notified_at") @@ -205,7 +193,6 @@ func (s *SQLStore) getSubscribersForBlock(db sq.BaseRunner, c store.Container, b if err != nil { s.logger.Error("Cannot fetch subscribers for block", mlog.String("block_id", blockID), - mlog.String("workspace_id", c.WorkspaceID), mlog.Err(err), ) return nil, err @@ -230,12 +217,11 @@ func (s *SQLStore) getSubscribersForBlock(db sq.BaseRunner, c store.Container, b } // getSubscribersCountForBlock returns a count of all subscribers for a block. -func (s *SQLStore) getSubscribersCountForBlock(db sq.BaseRunner, c store.Container, blockID string) (int, error) { +func (s *SQLStore) getSubscribersCountForBlock(db sq.BaseRunner, blockID string) (int, error) { query := s.getQueryBuilder(db). Select("count(subscriber_id)"). From(s.tablePrefix + "subscriptions"). Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}). Where(sq.Eq{"delete_at": 0}) row := query.QueryRow() @@ -245,7 +231,6 @@ func (s *SQLStore) getSubscribersCountForBlock(db sq.BaseRunner, c store.Contain if err != nil { s.logger.Error("Cannot count subscribers for block", mlog.String("block_id", blockID), - mlog.String("workspace_id", c.WorkspaceID), mlog.Err(err), ) return 0, err @@ -254,12 +239,11 @@ func (s *SQLStore) getSubscribersCountForBlock(db sq.BaseRunner, c store.Contain } // updateSubscribersNotifiedAt updates the notified_at field of all subscribers for a block. -func (s *SQLStore) updateSubscribersNotifiedAt(db sq.BaseRunner, c store.Container, blockID string, notifiedAt int64) error { +func (s *SQLStore) updateSubscribersNotifiedAt(db sq.BaseRunner, blockID string, notifiedAt int64) error { query := s.getQueryBuilder(db). Update(s.tablePrefix+"subscriptions"). Set("notified_at", notifiedAt). Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"workspace_id": c.WorkspaceID}). Where(sq.Eq{"delete_at": 0}) if _, err := query.Exec(); err != nil { diff --git a/server/services/store/sqlstore/system.go b/server/services/store/sqlstore/system.go index 2141ef370..bf743cb55 100644 --- a/server/services/store/sqlstore/system.go +++ b/server/services/store/sqlstore/system.go @@ -5,6 +5,7 @@ import ( "errors" sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/model" ) func (s *SQLStore) getSystemSetting(db sq.BaseRunner, key string) (string, error) { @@ -52,7 +53,7 @@ func (s *SQLStore) getSystemSettings(db sq.BaseRunner) (map[string]string, error func (s *SQLStore) setSystemSetting(db sq.BaseRunner, id, value string) error { query := s.getQueryBuilder(db).Insert(s.tablePrefix+"system_settings").Columns("id", "value").Values(id, value) - if s.dbType == mysqlDBType { + 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") diff --git a/server/services/store/sqlstore/team.go b/server/services/store/sqlstore/team.go new file mode 100644 index 000000000..abc604445 --- /dev/null +++ b/server/services/store/sqlstore/team.go @@ -0,0 +1,209 @@ +package sqlstore + +import ( + "database/sql" + "encoding/json" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/utils" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" + + sq "github.com/Masterminds/squirrel" +) + +var ( + teamFields = []string{ + "id", + "signup_token", + "COALESCE(settings, '{}')", + "modified_by", + "update_at", + } +) + +func (s *SQLStore) upsertTeamSignupToken(db sq.BaseRunner, team model.Team) error { + now := utils.GetMillis() + + query := s.getQueryBuilder(db). + Insert(s.tablePrefix+"teams"). + Columns( + "id", + "signup_token", + "modified_by", + "update_at", + ). + Values( + team.ID, + team.SignupToken, + team.ModifiedBy, + now, + ) + if s.dbType == model.MysqlDBType { + query = query.Suffix("ON DUPLICATE KEY UPDATE signup_token = ?, modified_by = ?, update_at = ?", + team.SignupToken, team.ModifiedBy, now) + } else { + query = query.Suffix( + `ON CONFLICT (id) + DO UPDATE SET signup_token = EXCLUDED.signup_token, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`, + ) + } + + _, err := query.Exec() + return err +} + +func (s *SQLStore) upsertTeamSettings(db sq.BaseRunner, team model.Team) error { + now := utils.GetMillis() + signupToken := utils.NewID(utils.IDTypeToken) + + settingsJSON, err := json.Marshal(team.Settings) + if err != nil { + return err + } + + query := s.getQueryBuilder(db). + Insert(s.tablePrefix+"teams"). + Columns( + "id", + "signup_token", + "settings", + "modified_by", + "update_at", + ). + Values( + team.ID, + signupToken, + settingsJSON, + team.ModifiedBy, + now, + ) + if s.dbType == model.MysqlDBType { + query = query.Suffix("ON DUPLICATE KEY UPDATE settings = ?, modified_by = ?, update_at = ?", settingsJSON, team.ModifiedBy, now) + } else { + query = query.Suffix( + `ON CONFLICT (id) + DO UPDATE SET settings = EXCLUDED.settings, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`, + ) + } + + _, err = query.Exec() + return err +} + +func (s *SQLStore) getTeam(db sq.BaseRunner, id string) (*model.Team, error) { + var settingsJSON string + + query := s.getQueryBuilder(db). + Select( + "id", + "signup_token", + "COALESCE(settings, '{}')", + "modified_by", + "update_at", + ). + From(s.tablePrefix + "teams"). + Where(sq.Eq{"id": id}) + row := query.QueryRow() + team := model.Team{} + + err := row.Scan( + &team.ID, + &team.SignupToken, + &settingsJSON, + &team.ModifiedBy, + &team.UpdateAt, + ) + if err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(settingsJSON), &team.Settings) + if err != nil { + s.logger.Error(`ERROR GetTeam settings json.Unmarshal`, mlog.Err(err)) + return nil, err + } + + return &team, nil +} + +func (s *SQLStore) getTeamsForUser(db sq.BaseRunner, _ string) ([]*model.Team, error) { + return s.getAllTeams(db) +} + +func (s *SQLStore) getTeamCount(db sq.BaseRunner) (int64, error) { + query := s.getQueryBuilder(db). + Select( + "COUNT(*) AS count", + ). + From(s.tablePrefix + "teams") + + rows, err := query.Query() + if err != nil { + s.logger.Error("ERROR GetTeamCount", mlog.Err(err)) + return 0, err + } + defer s.CloseRows(rows) + + var count int64 + + rows.Next() + err = rows.Scan(&count) + if err != nil { + s.logger.Error("Failed to fetch team count", mlog.Err(err)) + return 0, err + } + return count, nil +} + +func (s *SQLStore) teamsFromRows(rows *sql.Rows) ([]*model.Team, error) { + teams := []*model.Team{} + + for rows.Next() { + var team model.Team + var settingsBytes []byte + + err := rows.Scan( + &team.ID, + &team.SignupToken, + &settingsBytes, + &team.ModifiedBy, + &team.UpdateAt, + ) + if err != nil { + return nil, err + } + + err = json.Unmarshal(settingsBytes, &team.Settings) + if err != nil { + return nil, err + } + + teams = append(teams, &team) + } + + return teams, nil +} + +func (s *SQLStore) getAllTeams(db sq.BaseRunner) ([]*model.Team, error) { + query := s.getQueryBuilder(db). + Select(teamFields...). + From(s.tablePrefix + "teams") + rows, err := query.Query() + if err != nil { + s.logger.Error("ERROR GetAllTeams", mlog.Err(err)) + return nil, err + } + defer s.CloseRows(rows) + + teams, err := s.teamsFromRows(rows) + if err != nil { + return nil, err + } + + if len(teams) == 0 { + return nil, sql.ErrNoRows + } + + return teams, nil +} diff --git a/server/services/store/sqlstore/templates.go b/server/services/store/sqlstore/templates.go index 7eaae5cd3..a58ba15b4 100644 --- a/server/services/store/sqlstore/templates.go +++ b/server/services/store/sqlstore/templates.go @@ -15,25 +15,36 @@ var ( ) // removeDefaultTemplates deletes all the default templates and their children. -func (s *SQLStore) removeDefaultTemplates(db sq.BaseRunner, blocks []model.Block) error { +func (s *SQLStore) removeDefaultTemplates(db sq.BaseRunner, boards []*model.Board) error { count := 0 - for _, block := range blocks { + for _, board := range boards { + if board.CreatedBy != "system" { + continue + } // default template deletion does not need to go to blocks_history deleteQuery := s.getQueryBuilder(db). + Delete(s.tablePrefix + "boards"). + Where(sq.Eq{"id": board.ID}). + Where(sq.Eq{"is_template": true}) + + if _, err := deleteQuery.Exec(); err != nil { + return fmt.Errorf("cannot delete default template %s: %w", board.ID, err) + } + + deleteQuery = s.getQueryBuilder(db). Delete(s.tablePrefix + "blocks"). Where(sq.Or{ - sq.Eq{"id": block.ID}, - sq.Eq{"parent_id": block.ID}, - sq.Eq{"root_id": block.ID}, + sq.Eq{"parent_id": board.ID}, + sq.Eq{"root_id": board.ID}, + sq.Eq{"board_id": board.ID}, }) if _, err := deleteQuery.Exec(); err != nil { - return fmt.Errorf("cannot delete default template %s: %w", block.ID, err) + return fmt.Errorf("cannot delete default template %s: %w", board.ID, err) } s.logger.Trace("removed default template block", - mlog.String("block_id", block.ID), - mlog.String("block_type", string(block.Type)), + mlog.String("board_id", board.ID), ) count++ } @@ -43,31 +54,20 @@ func (s *SQLStore) removeDefaultTemplates(db sq.BaseRunner, blocks []model.Block return nil } -// getDefaultTemplateBlocks fetches all template blocks . -func (s *SQLStore) getDefaultTemplateBlocks(db sq.BaseRunner) ([]model.Block, error) { +// getDefaultTemplateBoards fetches all template blocks . +func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID string) ([]*model.Board, error) { query := s.getQueryBuilder(db). - Select(s.blockFields()...). - From(s.tablePrefix + "blocks"). - Where(sq.Eq{"coalesce(workspace_id, '0')": "0"}). - Where(sq.Eq{"created_by": "system"}) - - switch s.dbType { - case sqliteDBType: - query = query.Where(s.tablePrefix + "blocks.fields LIKE '%\"isTemplate\":true%'") - case mysqlDBType: - query = query.Where(s.tablePrefix + "blocks.fields LIKE '%\"isTemplate\":true%'") - case postgresDBType: - query = query.Where(s.tablePrefix + "blocks.fields ->> 'isTemplate' = 'true'") - default: - return nil, fmt.Errorf("cannot get default template blocks for database type %s: %w", s.dbType, ErrUnsupportedDatabaseType) - } + Select(boardFields("")...). + From(s.tablePrefix + "boards"). + Where(sq.Eq{"coalesce(team_id, '0')": teamID}). + Where(sq.Eq{"is_template": true}) rows, err := query.Query() if err != nil { - s.logger.Error(`isInitializationNeeded ERROR`, mlog.Err(err)) + s.logger.Error(`getTemplateBoards ERROR`, mlog.Err(err)) return nil, err } defer s.CloseRows(rows) - return s.blocksFromRows(rows) + return s.boardsFromRows(rows) } diff --git a/server/services/store/sqlstore/user.go b/server/services/store/sqlstore/user.go index 52edc438d..d4339feb0 100644 --- a/server/services/store/sqlstore/user.go +++ b/server/services/store/sqlstore/user.go @@ -4,12 +4,13 @@ import ( "database/sql" "encoding/json" "fmt" - "log" + + sq "github.com/Masterminds/squirrel" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/utils" - sq "github.com/Masterminds/squirrel" + "github.com/mattermost/mattermost-server/v6/shared/mlog" ) type UserNotFoundError struct { @@ -37,7 +38,7 @@ func (s *SQLStore) getRegisteredUserCount(db sq.BaseRunner) (int, error) { } func (s *SQLStore) getUserByCondition(db sq.BaseRunner, condition sq.Eq) (*model.User, error) { - users, err := s.getUsersByCondition(db, condition) + users, err := s.getUsersByCondition(db, condition, 0) if err != nil { return nil, err } @@ -49,7 +50,7 @@ func (s *SQLStore) getUserByCondition(db sq.BaseRunner, condition sq.Eq) (*model return users[0], nil } -func (s *SQLStore) getUsersByCondition(db sq.BaseRunner, condition sq.Eq) ([]*model.User, error) { +func (s *SQLStore) getUsersByCondition(db sq.BaseRunner, condition interface{}, limit uint64) ([]*model.User, error) { query := s.getQueryBuilder(db). Select( "id", @@ -67,9 +68,14 @@ func (s *SQLStore) getUsersByCondition(db sq.BaseRunner, condition sq.Eq) ([]*mo From(s.tablePrefix + "users"). Where(sq.Eq{"delete_at": 0}). Where(condition) + + if limit != 0 { + query = query.Limit(limit) + } + rows, err := query.Query() if err != nil { - log.Printf("getUsersByCondition ERROR: %v", err) + s.logger.Error(`getUsersByCondition ERROR`, mlog.Err(err)) return nil, err } defer s.CloseRows(rows) @@ -196,8 +202,12 @@ func (s *SQLStore) updateUserPasswordByID(db sq.BaseRunner, userID, password str return nil } -func (s *SQLStore) getUsersByWorkspace(db sq.BaseRunner, _ string) ([]*model.User, error) { - return s.getUsersByCondition(db, nil) +func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string) ([]*model.User, error) { + return s.getUsersByCondition(db, nil, 0) +} + +func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string) ([]*model.User, error) { + return s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10) } func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) { diff --git a/server/services/store/sqlstore/util.go b/server/services/store/sqlstore/util.go index 704984a27..5fa1fc604 100644 --- a/server/services/store/sqlstore/util.go +++ b/server/services/store/sqlstore/util.go @@ -3,9 +3,11 @@ package sqlstore import ( "database/sql" "fmt" + "io/ioutil" "os" "strings" + "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" @@ -25,22 +27,27 @@ func (s *SQLStore) IsErrNotFound(err error) bool { func PrepareNewTestDatabase() (dbType string, connectionString string, err error) { dbType = strings.TrimSpace(os.Getenv("FB_STORE_TEST_DB_TYPE")) if dbType == "" { - dbType = sqliteDBType + dbType = model.SqliteDBType } var dbName string var rootUser string - if dbType == sqliteDBType { - connectionString = ":memory:" + if dbType == model.SqliteDBType { + file, err := ioutil.TempFile("", "fbtest_*.db") + if err != nil { + return "", "", err + } + connectionString = file.Name() + _ = file.Close() } else if port := strings.TrimSpace(os.Getenv("FB_STORE_TEST_DOCKER_PORT")); port != "" { // docker unit tests take priority over any DSN env vars var template string switch dbType { - case mysqlDBType: + case model.MysqlDBType: template = "%s:mostest@tcp(localhost:%s)/%s?charset=utf8mb4,utf8&writeTimeout=30s" rootUser = "root" - case postgresDBType: + case model.PostgresDBType: template = "postgres://%s:mostest@localhost:%s/%s?sslmode=disable\u0026connect_timeout=10" rootUser = "mmuser" default: @@ -67,7 +74,7 @@ func PrepareNewTestDatabase() (dbType string, connectionString string, err error return "", "", fmt.Errorf("cannot create %s database %s: %w", dbType, dbName, err) } - if dbType != postgresDBType { + if dbType != model.PostgresDBType { _, err = sqlDB.Exec(fmt.Sprintf("GRANT ALL PRIVILEGES ON %s.* TO mmuser;", dbName)) if err != nil { return "", "", fmt.Errorf("cannot grant permissions on %s database %s: %w", dbType, dbName, err) diff --git a/server/services/store/sqlstore/workspaces.go b/server/services/store/sqlstore/workspaces.go deleted file mode 100644 index 4e77b68ed..000000000 --- a/server/services/store/sqlstore/workspaces.go +++ /dev/null @@ -1,162 +0,0 @@ -package sqlstore - -import ( - "encoding/json" - "errors" - "fmt" - - "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/utils" - - "github.com/mattermost/mattermost-server/v6/shared/mlog" - - sq "github.com/Masterminds/squirrel" -) - -var ( - errUnsupportedOperation = errors.New("unsupported operation") -) - -func (s *SQLStore) upsertWorkspaceSignupToken(db sq.BaseRunner, workspace model.Workspace) error { - now := utils.GetMillis() - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"workspaces"). - Columns( - "id", - "signup_token", - "modified_by", - "update_at", - ). - Values( - workspace.ID, - workspace.SignupToken, - workspace.ModifiedBy, - now, - ) - if s.dbType == mysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE signup_token = ?, modified_by = ?, update_at = ?", - workspace.SignupToken, workspace.ModifiedBy, now) - } else { - query = query.Suffix( - `ON CONFLICT (id) - DO UPDATE SET signup_token = EXCLUDED.signup_token, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`, - ) - } - - _, err := query.Exec() - return err -} - -func (s *SQLStore) upsertWorkspaceSettings(db sq.BaseRunner, workspace model.Workspace) error { - now := utils.GetMillis() - signupToken := utils.NewID(utils.IDTypeToken) - - settingsJSON, err := json.Marshal(workspace.Settings) - if err != nil { - return err - } - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"workspaces"). - Columns( - "id", - "signup_token", - "settings", - "modified_by", - "update_at", - ). - Values( - workspace.ID, - signupToken, - settingsJSON, - workspace.ModifiedBy, - now, - ) - if s.dbType == mysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE settings = ?, modified_by = ?, update_at = ?", settingsJSON, workspace.ModifiedBy, now) - } else { - query = query.Suffix( - `ON CONFLICT (id) - DO UPDATE SET settings = EXCLUDED.settings, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`, - ) - } - - _, err = query.Exec() - return err -} - -func (s *SQLStore) getWorkspace(db sq.BaseRunner, id string) (*model.Workspace, error) { - var settingsJSON string - - query := s.getQueryBuilder(db). - Select( - "id", - "signup_token", - "COALESCE(settings, '{}')", - "modified_by", - "update_at", - ). - From(s.tablePrefix + "workspaces"). - Where(sq.Eq{"id": id}) - row := query.QueryRow() - workspace := model.Workspace{} - - err := row.Scan( - &workspace.ID, - &workspace.SignupToken, - &settingsJSON, - &workspace.ModifiedBy, - &workspace.UpdateAt, - ) - if err != nil { - return nil, err - } - - err = json.Unmarshal([]byte(settingsJSON), &workspace.Settings) - if err != nil { - s.logger.Error(`ERROR GetWorkspace settings json.Unmarshal`, mlog.Err(err)) - return nil, err - } - - return &workspace, nil -} - -func (s *SQLStore) hasWorkspaceAccess(db sq.BaseRunner, userID string, workspaceID string) (bool, error) { - return true, nil -} - -func (s *SQLStore) getWorkspaceCount(db sq.BaseRunner) (int64, error) { - query := s.getQueryBuilder(db). - Select( - "COUNT(*) AS count", - ). - From(s.tablePrefix + "workspaces") - - rows, err := query.Query() - if err != nil { - s.logger.Error("ERROR GetWorkspaceCount", mlog.Err(err)) - return 0, err - } - defer s.CloseRows(rows) - - var count int64 - - rows.Next() - err = rows.Scan(&count) - if err != nil { - s.logger.Error("Failed to fetch workspace count", mlog.Err(err)) - return 0, err - } - return count, nil -} - -func (s *SQLStore) getUserWorkspaces(_ sq.BaseRunner, _ string) ([]model.UserWorkspace, error) { - return nil, fmt.Errorf("GetUserWorkspaces %w", errUnsupportedOperation) -} - -func (s *SQLStore) createPrivateWorkspace(_ sq.BaseRunner, _ string) (string, error) { - // for personal server we always have only - // a single workspace, with id "0". - return "0", nil -} diff --git a/server/services/store/store.go b/server/services/store/store.go index 9d2280d2a..28b63eb03 100644 --- a/server/services/store/store.go +++ b/server/services/store/store.go @@ -10,40 +10,36 @@ import ( "github.com/mattermost/focalboard/server/model" ) -// Conainer represents a container in a store -// Using a struct to make extending this easier in the future. -type Container struct { - WorkspaceID string -} - // Store represents the abstraction of the data storage. type Store interface { - GetBlocksWithParentAndType(c Container, parentID string, blockType string) ([]model.Block, error) - GetBlocksWithParent(c Container, parentID string) ([]model.Block, error) - GetBlocksWithRootID(c Container, rootID string) ([]model.Block, error) - GetBlocksWithType(c Container, blockType string) ([]model.Block, error) - GetSubTree2(c Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) - GetSubTree3(c Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) - GetAllBlocks(c Container) ([]model.Block, error) - GetRootID(c Container, blockID string) (string, error) - GetParentID(c Container, blockID string) (string, error) + GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]model.Block, error) + GetBlocksWithParent(boardID, parentID string) ([]model.Block, error) + GetBlocksWithBoardID(boardID string) ([]model.Block, error) + GetBlocksWithType(boardID, blockType string) ([]model.Block, error) + GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) + GetSubTree3(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) + GetBlocksForBoard(boardID string) ([]model.Block, error) // @withTransaction - InsertBlock(c Container, block *model.Block, userID string) error + InsertBlock(block *model.Block, userID string) error // @withTransaction - InsertBlocks(c Container, blocks []model.Block, userID string) error + DeleteBlock(blockID string, modifiedBy string) error // @withTransaction - DeleteBlock(c Container, blockID string, modifiedBy string) error + InsertBlocks(blocks []model.Block, userID string) error // @withTransaction - UndeleteBlock(c Container, blockID string, modifiedBy string) error + UndeleteBlock(blockID string, modifiedBy string) error GetBlockCountsByType() (map[string]int64, error) - GetBlock(c Container, blockID string) (*model.Block, error) + GetBlock(blockID string) (*model.Block, error) // @withTransaction - PatchBlock(c Container, blockID string, blockPatch *model.BlockPatch, userID string) error - GetBlockHistory(c Container, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) - GetBoardAndCardByID(c Container, blockID string) (board *model.Block, card *model.Block, err error) - GetBoardAndCard(c Container, block *model.Block) (board *model.Block, card *model.Block, err error) + PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error + GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) + GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) + GetBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) // @withTransaction - PatchBlocks(c Container, blockPatches *model.BlockPatchBatch, userID string) error + DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) + // @withTransaction + DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) + // @withTransaction + PatchBlocks(blockPatches *model.BlockPatchBatch, userID string) error Shutdown() error @@ -59,7 +55,8 @@ type Store interface { UpdateUser(user *model.User) error UpdateUserPassword(username, password string) error UpdateUserPasswordByID(userID, password string) error - GetUsersByWorkspace(workspaceID string) ([]*model.User, error) + GetUsersByTeam(teamID string) ([]*model.User, error) + SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) PatchUserProps(userID string, patch model.UserPropPatch) error GetActiveUserCount(updatedSecondsAgo int64) (int, error) @@ -70,32 +67,65 @@ type Store interface { DeleteSession(sessionID string) error CleanUpSessions(expireTime int64) error - UpsertSharing(c Container, sharing model.Sharing) error - GetSharing(c Container, rootID string) (*model.Sharing, error) + UpsertSharing(sharing model.Sharing) error + GetSharing(rootID string) (*model.Sharing, error) - UpsertWorkspaceSignupToken(workspace model.Workspace) error - UpsertWorkspaceSettings(workspace model.Workspace) error - GetWorkspace(ID string) (*model.Workspace, error) - HasWorkspaceAccess(userID string, workspaceID string) (bool, error) - GetWorkspaceCount() (int64, error) - GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) - CreatePrivateWorkspace(userID string) (string, error) + UpsertTeamSignupToken(team model.Team) error + UpsertTeamSettings(team model.Team) error + GetTeam(ID string) (*model.Team, error) + GetTeamsForUser(userID string) ([]*model.Team, error) + GetAllTeams() ([]*model.Team, error) + GetTeamCount() (int64, error) - CreateSubscription(c Container, sub *model.Subscription) (*model.Subscription, error) - DeleteSubscription(c Container, blockID string, subscriberID string) error - GetSubscription(c Container, blockID string, subscriberID string) (*model.Subscription, error) - GetSubscriptions(c Container, subscriberID string) ([]*model.Subscription, error) - GetSubscribersForBlock(c Container, blockID string) ([]*model.Subscriber, error) - GetSubscribersCountForBlock(c Container, blockID string) (int, error) - UpdateSubscribersNotifiedAt(c Container, blockID string, notifiedAt int64) error + InsertBoard(board *model.Board, userID string) (*model.Board, error) + // @withTransaction + InsertBoardWithAdmin(board *model.Board, userID string) (*model.Board, *model.BoardMember, error) + // @withTransaction + PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) + GetBoard(id string) (*model.Board, error) + GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) + // @withTransaction + DeleteBoard(boardID, userID string) error + + SaveMember(bm *model.BoardMember) (*model.BoardMember, error) + DeleteMember(boardID, userID string) error + GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) + GetMembersForBoard(boardID string) ([]*model.BoardMember, error) + GetMembersForUser(userID string) ([]*model.BoardMember, error) + SearchBoardsForUserAndTeam(term, userID, teamID string) ([]*model.Board, error) + + // @withTransaction + CreateBoardsAndBlocksWithAdmin(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) + // @withTransaction + CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) + // @withTransaction + PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) + // @withTransaction + DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error + + GetCategory(id string) (*model.Category, error) + CreateCategory(category model.Category) error + UpdateCategory(category model.Category) error + DeleteCategory(categoryID, userID, teamID string) error + + GetUserCategoryBlocks(userID, teamID string) ([]model.CategoryBlocks, error) + AddUpdateCategoryBlock(userID, categoryID, blockID string) error + + CreateSubscription(sub *model.Subscription) (*model.Subscription, error) + DeleteSubscription(blockID string, subscriberID string) error + GetSubscription(blockID string, subscriberID string) (*model.Subscription, error) + GetSubscriptions(subscriberID string) ([]*model.Subscription, error) + GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) + GetSubscribersCountForBlock(blockID string) (int, error) + UpdateSubscribersNotifiedAt(blockID string, notifiedAt int64) error UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) - DeleteNotificationHint(c Container, blockID string) error - GetNotificationHint(c Container, blockID string) (*model.NotificationHint, error) + DeleteNotificationHint(blockID string) error + GetNotificationHint(blockID string) (*model.NotificationHint, error) GetNextNotificationHint(remove bool) (*model.NotificationHint, error) - RemoveDefaultTemplates(blocks []model.Block) error - GetDefaultTemplateBlocks() ([]model.Block, error) + RemoveDefaultTemplates(boards []*model.Board) error + GetTemplateBoards(teamID string) ([]*model.Board, error) DBType() string diff --git a/server/services/store/storetests/blocks.go b/server/services/store/storetests/blocks.go index ed906d52c..b889e433d 100644 --- a/server/services/store/storetests/blocks.go +++ b/server/services/store/storetests/blocks.go @@ -4,98 +4,98 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" + + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( - testUserID = "user-id" + testUserID = "user-id" + testTeamID = "team-id" + testBoardID = "board-id" ) +//nolint:dupl func StoreTestBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { - container := store.Container{ - WorkspaceID: "0", - } - t.Run("InsertBlock", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testInsertBlock(t, store, container) + testInsertBlock(t, store) }) t.Run("InsertBlocks", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testInsertBlocks(t, store, container) + testInsertBlocks(t, store) }) t.Run("PatchBlock", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testPatchBlock(t, store, container) + testPatchBlock(t, store) }) t.Run("PatchBlocks", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testPatchBlocks(t, store, container) + testPatchBlocks(t, store) }) t.Run("DeleteBlock", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testDeleteBlock(t, store, container) + testDeleteBlock(t, store) }) t.Run("UndeleteBlock", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testUndeleteBlock(t, store, container) + testUndeleteBlock(t, store) }) t.Run("GetSubTree2", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetSubTree2(t, store, container) + testGetSubTree2(t, store) }) t.Run("GetSubTree3", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetSubTree3(t, store, container) - }) - t.Run("GetParentID", func(t *testing.T) { - store, tearDown := setup(t) - defer tearDown() - testGetParents(t, store, container) + testGetSubTree3(t, store) }) t.Run("GetBlocks", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetBlocks(t, store, container) + testGetBlocks(t, store) }) t.Run("GetBlock", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetBlock(t, store, container) + testGetBlock(t, store) + }) + t.Run("DuplicateBlock", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testDuplicateBlock(t, store) }) } -func testInsertBlock(t *testing.T, store store.Store, container store.Container) { +func testInsertBlock(t *testing.T, store store.Store) { userID := testUserID + boardID := testBoardID - blocks, errBlocks := store.GetAllBlocks(container) + blocks, errBlocks := store.GetBlocksForBoard(boardID) require.NoError(t, errBlocks) initialCount := len(blocks) t.Run("valid block", func(t *testing.T) { block := model.Block{ ID: "id-test", - RootID: "id-test", + BoardID: boardID, ModifiedBy: userID, } - err := store.InsertBlock(container, &block, "user-id-1") + err := store.InsertBlock(&block, "user-id-1") require.NoError(t, err) - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount+1) }) @@ -103,14 +103,14 @@ func testInsertBlock(t *testing.T, store store.Store, container store.Container) t.Run("invalid rootid", func(t *testing.T) { block := model.Block{ ID: "id-test", - RootID: "", + BoardID: "", ModifiedBy: userID, } - err := store.InsertBlock(container, &block, "user-id-1") + err := store.InsertBlock(&block, "user-id-1") require.Error(t, err) - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount+1) }) @@ -118,38 +118,38 @@ func testInsertBlock(t *testing.T, store store.Store, container store.Container) t.Run("invalid fields data", func(t *testing.T) { block := model.Block{ ID: "id-test", - RootID: "id-test", + BoardID: "id-test", ModifiedBy: userID, Fields: map[string]interface{}{"no-serialiable-value": t.Run}, } - err := store.InsertBlock(container, &block, "user-id-1") + err := store.InsertBlock(&block, "user-id-1") require.Error(t, err) - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount+1) }) t.Run("insert new block", func(t *testing.T) { block := model.Block{ - RootID: "root-id", + BoardID: testBoardID, } - err := store.InsertBlock(container, &block, "user-id-2") + err := store.InsertBlock(&block, "user-id-2") require.NoError(t, err) require.Equal(t, "user-id-2", block.CreatedBy) }) t.Run("update existing block", func(t *testing.T) { block := model.Block{ - ID: "id-2", - RootID: "root-id", - Title: "Old Title", + ID: "id-2", + BoardID: "board-id-1", + Title: "Old Title", } // inserting - err := store.InsertBlock(container, &block, "user-id-2") + err := store.InsertBlock(&block, "user-id-2") require.NoError(t, err) // created by populated from user id for new blocks @@ -157,16 +157,16 @@ func testInsertBlock(t *testing.T, store store.Store, container store.Container) // hack to avoid multiple, quick updates to a card // violating block_history composite primary key constraint - time.Sleep(1 * time.Second) + time.Sleep(1 * time.Millisecond) // updating newBlock := model.Block{ ID: "id-2", - RootID: "root-id", + BoardID: "board-id-1", CreatedBy: "user-id-3", Title: "New Title", } - err = store.InsertBlock(container, &newBlock, "user-id-4") + err = store.InsertBlock(&newBlock, "user-id-4") require.NoError(t, err) // created by is not altered for existing blocks require.Equal(t, "user-id-3", newBlock.CreatedBy) @@ -182,7 +182,7 @@ func testInsertBlock(t *testing.T, store store.Store, container store.Container) t.Run("data tamper attempt", func(t *testing.T) { block := model.Block{ ID: "id-10", - RootID: "root-id", + BoardID: "board-id-1", Title: "Old Title", CreateAt: utils.GetMillisForTime(createdAt), UpdateAt: utils.GetMillisForTime(updateAt), @@ -191,12 +191,13 @@ func testInsertBlock(t *testing.T, store store.Store, container store.Container) } // inserting - err := store.InsertBlock(container, &block, "user-id-1") + err := store.InsertBlock(&block, "user-id-1") require.NoError(t, err) - retrievedBlock, err := store.GetBlock(container, "id-10") + retrievedBlock, err := store.GetBlock("id-10") assert.NoError(t, err) assert.NotNil(t, retrievedBlock) + assert.Equal(t, "board-id-1", retrievedBlock.BoardID) assert.Equal(t, "user-id-1", retrievedBlock.CreatedBy) assert.Equal(t, "user-id-1", retrievedBlock.ModifiedBy) assert.WithinDurationf(t, time.Now(), utils.GetTimeForMillis(retrievedBlock.CreateAt), 1*time.Second, "create time should be current time") @@ -204,76 +205,77 @@ func testInsertBlock(t *testing.T, store store.Store, container store.Container) }) } -func testInsertBlocks(t *testing.T, store store.Store, container store.Container) { +func testInsertBlocks(t *testing.T, store store.Store) { userID := testUserID - blocks, errBlocks := store.GetAllBlocks(container) + blocks, errBlocks := store.GetBlocksForBoard("id-test") require.NoError(t, errBlocks) initialCount := len(blocks) t.Run("invalid block", func(t *testing.T) { validBlock := model.Block{ ID: "id-test", - RootID: "id-test", + BoardID: "id-test", ModifiedBy: userID, } invalidBlock := model.Block{ ID: "id-test", - RootID: "", + BoardID: "", ModifiedBy: userID, } newBlocks := []model.Block{validBlock, invalidBlock} time.Sleep(1 * time.Millisecond) - err := store.InsertBlocks(container, newBlocks, "user-id-1") + err := store.InsertBlocks(newBlocks, "user-id-1") require.Error(t, err) - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard("id-test") require.NoError(t, err) // no blocks should have been inserted require.Len(t, blocks, initialCount) }) } -func testPatchBlock(t *testing.T, store store.Store, container store.Container) { +func testPatchBlock(t *testing.T, store store.Store) { userID := testUserID + boardID := "board-id-1" block := model.Block{ ID: "id-test", - RootID: "id-test", + BoardID: boardID, Title: "oldTitle", ModifiedBy: userID, Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"}, } - err := store.InsertBlock(container, &block, "user-id-1") + err := store.InsertBlock(&block, "user-id-1") require.NoError(t, err) - blocks, errBlocks := store.GetAllBlocks(container) + blocks, errBlocks := store.GetBlocksForBoard(boardID) require.NoError(t, errBlocks) initialCount := len(blocks) - t.Run("not existing block", func(t *testing.T) { - err := store.PatchBlock(container, "invalid-block-id", &model.BlockPatch{}, "user-id-1") + t.Run("not existing block id", func(t *testing.T) { + err := store.PatchBlock("invalid-block-id", &model.BlockPatch{}, "user-id-1") require.Error(t, err) - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount) }) t.Run("invalid rootid", func(t *testing.T) { - wrongRootID := "" + wrongBoardID := "" blockPatch := model.BlockPatch{ - RootID: &wrongRootID, + BoardID: &wrongBoardID, } - err := store.PatchBlock(container, "id-test", &blockPatch, "user-id-1") + err := store.PatchBlock("id-test", &blockPatch, "user-id-1") require.Error(t, err) - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount) }) @@ -283,10 +285,10 @@ func testPatchBlock(t *testing.T, store store.Store, container store.Container) UpdatedFields: map[string]interface{}{"no-serialiable-value": t.Run}, } - err := store.PatchBlock(container, "id-test", &blockPatch, "user-id-1") + err := store.PatchBlock("id-test", &blockPatch, "user-id-1") require.Error(t, err) - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount) }) @@ -301,10 +303,10 @@ func testPatchBlock(t *testing.T, store store.Store, container store.Container) time.Sleep(1 * time.Millisecond) // inserting - err := store.PatchBlock(container, "id-test", &blockPatch, "user-id-2") + err := store.PatchBlock("id-test", &blockPatch, "user-id-2") require.NoError(t, err) - retrievedBlock, err := store.GetBlock(container, "id-test") + retrievedBlock, err := store.GetBlock("id-test") require.NoError(t, err) // created by populated from user id for new blocks @@ -321,10 +323,10 @@ func testPatchBlock(t *testing.T, store store.Store, container store.Container) time.Sleep(1 * time.Millisecond) // inserting - err := store.PatchBlock(container, "id-test", &blockPatch, "user-id-2") + err := store.PatchBlock("id-test", &blockPatch, "user-id-2") require.NoError(t, err) - retrievedBlock, err := store.GetBlock(container, "id-test") + retrievedBlock, err := store.GetBlock("id-test") require.NoError(t, err) // created by populated from user id for new blocks @@ -343,10 +345,10 @@ func testPatchBlock(t *testing.T, store store.Store, container store.Container) time.Sleep(1 * time.Millisecond) // inserting - err := store.PatchBlock(container, "id-test", &blockPatch, "user-id-2") + err := store.PatchBlock("id-test", &blockPatch, "user-id-2") require.NoError(t, err) - retrievedBlock, err := store.GetBlock(container, "id-test") + retrievedBlock, err := store.GetBlock("id-test") require.NoError(t, err) // created by populated from user id for new blocks @@ -357,21 +359,21 @@ func testPatchBlock(t *testing.T, store store.Store, container store.Container) }) } -func testPatchBlocks(t *testing.T, store store.Store, container store.Container) { +func testPatchBlocks(t *testing.T, store store.Store) { block := model.Block{ - ID: "id-test", - RootID: "id-test", - Title: "oldTitle", + ID: "id-test", + BoardID: "id-test", + Title: "oldTitle", } block2 := model.Block{ - ID: "id-test2", - RootID: "id-test2", - Title: "oldTitle2", + ID: "id-test2", + BoardID: "id-test2", + Title: "oldTitle2", } insertBlocks := []model.Block{block, block2} - err := store.InsertBlocks(container, insertBlocks, "user-id-1") + err := store.InsertBlocks(insertBlocks, "user-id-1") require.NoError(t, err) t.Run("successful updated existing blocks", func(t *testing.T) { @@ -388,20 +390,20 @@ func testPatchBlocks(t *testing.T, store store.Store, container store.Container) blockPatches := []model.BlockPatch{blockPatch, blockPatch2} time.Sleep(1 * time.Millisecond) - err := store.PatchBlocks(container, &model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1") + err := store.PatchBlocks(&model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1") require.NoError(t, err) - retrievedBlock, err := store.GetBlock(container, "id-test") + retrievedBlock, err := store.GetBlock("id-test") require.NoError(t, err) require.Equal(t, title, retrievedBlock.Title) - retrievedBlock2, err := store.GetBlock(container, "id-test2") + retrievedBlock2, err := store.GetBlock("id-test2") require.NoError(t, err) require.Equal(t, title, retrievedBlock2.Title) }) t.Run("invalid block id, nothing updated existing blocks", func(t *testing.T) { - if store.DBType() == "sqlite3" { + if store.DBType() == model.SqliteDBType { t.Skip("No transactions support int sqlite") } @@ -417,10 +419,11 @@ func testPatchBlocks(t *testing.T, store store.Store, container store.Container) blockIds := []string{"id-test", "invalid id"} blockPatches := []model.BlockPatch{blockPatch, blockPatch2} - err := store.PatchBlocks(container, &model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1") + time.Sleep(1 * time.Millisecond) + err := store.PatchBlocks(&model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1") require.Error(t, err) - retrievedBlock, err := store.GetBlock(container, "id-test") + retrievedBlock, err := store.GetBlock("id-test") require.NoError(t, err) require.NotEqual(t, title, retrievedBlock.Title) }) @@ -430,56 +433,58 @@ var ( subtreeSampleBlocks = []model.Block{ { ID: "parent", - RootID: "parent", + BoardID: testBoardID, ModifiedBy: testUserID, }, { ID: "child1", - RootID: "parent", + BoardID: testBoardID, ParentID: "parent", ModifiedBy: testUserID, }, { ID: "child2", - RootID: "parent", + BoardID: testBoardID, ParentID: "parent", ModifiedBy: testUserID, }, { ID: "grandchild1", - RootID: "parent", + BoardID: testBoardID, ParentID: "child1", ModifiedBy: testUserID, }, { ID: "grandchild2", - RootID: "parent", + BoardID: testBoardID, ParentID: "child2", ModifiedBy: testUserID, }, { ID: "greatgrandchild1", - RootID: "parent", + BoardID: testBoardID, ParentID: "grandchild1", ModifiedBy: testUserID, }, } ) -func testGetSubTree2(t *testing.T, store store.Store, container store.Container) { - blocks, err := store.GetAllBlocks(container) +func testGetSubTree2(t *testing.T, store store.Store) { + boardID := testBoardID + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) initialCount := len(blocks) - InsertBlocks(t, store, container, subtreeSampleBlocks, "user-id-1") - defer DeleteBlocks(t, store, container, subtreeSampleBlocks, "test") + InsertBlocks(t, store, subtreeSampleBlocks, "user-id-1") + time.Sleep(1 * time.Millisecond) + defer DeleteBlocks(t, store, subtreeSampleBlocks, "test") - blocks, err = store.GetAllBlocks(container) + blocks, err = store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount+6) t.Run("from root id", func(t *testing.T) { - blocks, err = store.GetSubTree2(container, "parent", model.QuerySubtreeOptions{}) + blocks, err = store.GetSubTree2(boardID, "parent", model.QuerySubtreeOptions{}) require.NoError(t, err) require.Len(t, blocks, 3) require.True(t, ContainsBlockWithID(blocks, "parent")) @@ -488,7 +493,7 @@ func testGetSubTree2(t *testing.T, store store.Store, container store.Container) }) t.Run("from child id", func(t *testing.T) { - blocks, err = store.GetSubTree2(container, "child1", model.QuerySubtreeOptions{}) + blocks, err = store.GetSubTree2(boardID, "child1", model.QuerySubtreeOptions{}) require.NoError(t, err) require.Len(t, blocks, 2) require.True(t, ContainsBlockWithID(blocks, "child1")) @@ -496,26 +501,28 @@ func testGetSubTree2(t *testing.T, store store.Store, container store.Container) }) t.Run("from not existing id", func(t *testing.T) { - blocks, err = store.GetSubTree2(container, "not-exists", model.QuerySubtreeOptions{}) + blocks, err = store.GetSubTree2(boardID, "not-exists", model.QuerySubtreeOptions{}) require.NoError(t, err) require.Len(t, blocks, 0) }) } -func testGetSubTree3(t *testing.T, store store.Store, container store.Container) { - blocks, err := store.GetAllBlocks(container) +func testGetSubTree3(t *testing.T, store store.Store) { + boardID := testBoardID + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) initialCount := len(blocks) - InsertBlocks(t, store, container, subtreeSampleBlocks, "user-id-1") - defer DeleteBlocks(t, store, container, subtreeSampleBlocks, "test") + InsertBlocks(t, store, subtreeSampleBlocks, "user-id-1") + time.Sleep(1 * time.Millisecond) + defer DeleteBlocks(t, store, subtreeSampleBlocks, "test") - blocks, err = store.GetAllBlocks(container) + blocks, err = store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount+6) - t.Run("from root id", func(t *testing.T) { - blocks, err = store.GetSubTree3(container, "parent", model.QuerySubtreeOptions{}) + t.Run("from board id", func(t *testing.T) { + blocks, err = store.GetSubTree3(boardID, "parent", model.QuerySubtreeOptions{}) require.NoError(t, err) require.Len(t, blocks, 5) require.True(t, ContainsBlockWithID(blocks, "parent")) @@ -526,7 +533,7 @@ func testGetSubTree3(t *testing.T, store store.Store, container store.Container) }) t.Run("from child id", func(t *testing.T) { - blocks, err = store.GetSubTree3(container, "child1", model.QuerySubtreeOptions{}) + blocks, err = store.GetSubTree3(boardID, "child1", model.QuerySubtreeOptions{}) require.NoError(t, err) require.Len(t, blocks, 3) require.True(t, ContainsBlockWithID(blocks, "child1")) @@ -535,190 +542,146 @@ func testGetSubTree3(t *testing.T, store store.Store, container store.Container) }) t.Run("from not existing id", func(t *testing.T) { - blocks, err = store.GetSubTree3(container, "not-exists", model.QuerySubtreeOptions{}) + blocks, err = store.GetSubTree3(boardID, "not-exists", model.QuerySubtreeOptions{}) require.NoError(t, err) require.Len(t, blocks, 0) }) } -func testGetParents(t *testing.T, store store.Store, container store.Container) { - blocks, err := store.GetAllBlocks(container) - require.NoError(t, err) - initialCount := len(blocks) - - InsertBlocks(t, store, container, subtreeSampleBlocks, "user-id-1") - defer DeleteBlocks(t, store, container, subtreeSampleBlocks, "test") - - blocks, err = store.GetAllBlocks(container) - require.NoError(t, err) - require.Len(t, blocks, initialCount+6) - - t.Run("root from root id", func(t *testing.T) { - rootID, err := store.GetRootID(container, "parent") - require.NoError(t, err) - require.Equal(t, "parent", rootID) - }) - - t.Run("root from child id", func(t *testing.T) { - rootID, err := store.GetRootID(container, "child1") - require.NoError(t, err) - require.Equal(t, "parent", rootID) - }) - - t.Run("root from not existing id", func(t *testing.T) { - _, err := store.GetRootID(container, "not-exists") - require.Error(t, err) - }) - - t.Run("parent from root id", func(t *testing.T) { - parentID, err := store.GetParentID(container, "parent") - require.NoError(t, err) - require.Equal(t, "", parentID) - }) - - t.Run("parent from child id", func(t *testing.T) { - parentID, err := store.GetParentID(container, "grandchild1") - require.NoError(t, err) - require.Equal(t, "child1", parentID) - }) - - t.Run("parent from not existing id", func(t *testing.T) { - _, err := store.GetParentID(container, "not-exists") - require.Error(t, err) - }) -} - -func testDeleteBlock(t *testing.T, store store.Store, container store.Container) { +func testDeleteBlock(t *testing.T, store store.Store) { userID := testUserID + boardID := testBoardID - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) initialCount := len(blocks) blocksToInsert := []model.Block{ { ID: "block1", - RootID: "block1", + BoardID: boardID, ModifiedBy: userID, }, { ID: "block2", - RootID: "block2", + BoardID: boardID, ModifiedBy: userID, }, { ID: "block3", - RootID: "block3", + BoardID: boardID, ModifiedBy: userID, }, } - InsertBlocks(t, store, container, blocksToInsert, "user-id-1") - defer DeleteBlocks(t, store, container, blocksToInsert, "test") + InsertBlocks(t, store, blocksToInsert, "user-id-1") + defer DeleteBlocks(t, store, blocksToInsert, "test") - blocks, err = store.GetAllBlocks(container) + blocks, err = store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount+3) - t.Run("exiting id", func(t *testing.T) { + t.Run("existing id", func(t *testing.T) { // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock(container, "block1", userID) + err := store.DeleteBlock("block1", userID) require.NoError(t, err) }) - t.Run("exiting id multiple times", func(t *testing.T) { + t.Run("existing id multiple times", func(t *testing.T) { // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock(container, "block1", userID) + err := store.DeleteBlock("block1", userID) require.NoError(t, err) // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err = store.DeleteBlock(container, "block1", userID) + err = store.DeleteBlock("block1", userID) require.NoError(t, err) }) t.Run("from not existing id", func(t *testing.T) { // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock(container, "not-exists", userID) + err := store.DeleteBlock("not-exists", userID) require.NoError(t, err) }) } -func testUndeleteBlock(t *testing.T, store store.Store, container store.Container) { +func testUndeleteBlock(t *testing.T, store store.Store) { + boardID := testBoardID userID := testUserID - blocks, err := store.GetAllBlocks(container) + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) initialCount := len(blocks) blocksToInsert := []model.Block{ { ID: "block1", - RootID: "block1", + BoardID: boardID, ModifiedBy: userID, }, { ID: "block2", - RootID: "block2", + BoardID: boardID, ModifiedBy: userID, }, { ID: "block3", - RootID: "block3", + BoardID: boardID, ModifiedBy: userID, }, } - InsertBlocks(t, store, container, blocksToInsert, "user-id-1") - defer DeleteBlocks(t, store, container, blocksToInsert, "test") + InsertBlocks(t, store, blocksToInsert, "user-id-1") + defer DeleteBlocks(t, store, blocksToInsert, "test") - blocks, err = store.GetAllBlocks(container) + blocks, err = store.GetBlocksForBoard(boardID) require.NoError(t, err) require.Len(t, blocks, initialCount+3) - t.Run("exiting id", func(t *testing.T) { + t.Run("existing id", func(t *testing.T) { // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock(container, "block1", userID) + err := store.DeleteBlock("block1", userID) require.NoError(t, err) - block, err := store.GetBlock(container, "block1") + block, err := store.GetBlock("block1") require.NoError(t, err) require.Nil(t, block) - err = store.UndeleteBlock(container, "block1", userID) + time.Sleep(1 * time.Millisecond) + err = store.UndeleteBlock("block1", userID) require.NoError(t, err) - block, err = store.GetBlock(container, "block1") + block, err = store.GetBlock("block1") require.NoError(t, err) require.NotNil(t, block) }) - t.Run("exiting id multiple times", func(t *testing.T) { + t.Run("existing id multiple times", func(t *testing.T) { // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock(container, "block1", userID) + err := store.DeleteBlock("block1", userID) require.NoError(t, err) - block, err := store.GetBlock(container, "block1") + block, err := store.GetBlock("block1") require.NoError(t, err) require.Nil(t, block) // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err = store.UndeleteBlock(container, "block1", userID) + err = store.UndeleteBlock("block1", userID) require.NoError(t, err) - block, err = store.GetBlock(container, "block1") + block, err = store.GetBlock("block1") require.NoError(t, err) require.NotNil(t, block) // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err = store.UndeleteBlock(container, "block1", userID) + err = store.UndeleteBlock("block1", userID) require.NoError(t, err) - block, err = store.GetBlock(container, "block1") + block, err = store.GetBlock("block1") require.NoError(t, err) require.NotNil(t, block) }) @@ -726,139 +689,140 @@ func testUndeleteBlock(t *testing.T, store store.Store, container store.Containe t.Run("from not existing id", func(t *testing.T) { // Wait for not colliding the ID+insert_at key time.Sleep(1 * time.Millisecond) - err := store.UndeleteBlock(container, "not-exists", userID) + err := store.UndeleteBlock("not-exists", userID) require.NoError(t, err) - block, err := store.GetBlock(container, "not-exists") + block, err := store.GetBlock("not-exists") require.NoError(t, err) require.Nil(t, block) }) } -func testGetBlocks(t *testing.T, store store.Store, container store.Container) { - blocks, err := store.GetAllBlocks(container) +func testGetBlocks(t *testing.T, store store.Store) { + boardID := testBoardID + blocks, err := store.GetBlocksForBoard(boardID) require.NoError(t, err) blocksToInsert := []model.Block{ { ID: "block1", + BoardID: boardID, ParentID: "", - RootID: "block1", ModifiedBy: testUserID, Type: "test", }, { ID: "block2", + BoardID: boardID, ParentID: "block1", - RootID: "block1", ModifiedBy: testUserID, Type: "test", }, { ID: "block3", + BoardID: boardID, ParentID: "block1", - RootID: "block1", ModifiedBy: testUserID, Type: "test", }, { ID: "block4", + BoardID: boardID, ParentID: "block1", - RootID: "block1", ModifiedBy: testUserID, Type: "test2", }, { ID: "block5", + BoardID: boardID, ParentID: "block2", - RootID: "block2", ModifiedBy: testUserID, Type: "test", }, } - InsertBlocks(t, store, container, blocksToInsert, "user-id-1") - defer DeleteBlocks(t, store, container, blocksToInsert, "test") + InsertBlocks(t, store, blocksToInsert, "user-id-1") + defer DeleteBlocks(t, store, blocksToInsert, "test") t.Run("not existing parent", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithParentAndType(container, "not-exists", "test") + blocks, err = store.GetBlocksWithParentAndType(boardID, "not-exists", "test") require.NoError(t, err) require.Len(t, blocks, 0) }) t.Run("not existing type", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithParentAndType(container, "block1", "not-existing") + blocks, err = store.GetBlocksWithParentAndType(boardID, "block1", "not-existing") require.NoError(t, err) require.Len(t, blocks, 0) }) t.Run("valid parent and type", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithParentAndType(container, "block1", "test") + blocks, err = store.GetBlocksWithParentAndType(boardID, "block1", "test") require.NoError(t, err) require.Len(t, blocks, 2) }) t.Run("not existing parent", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithParent(container, "not-exists") + blocks, err = store.GetBlocksWithParent(boardID, "not-exists") require.NoError(t, err) require.Len(t, blocks, 0) }) t.Run("valid parent", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithParent(container, "block1") + blocks, err = store.GetBlocksWithParent(boardID, "block1") require.NoError(t, err) require.Len(t, blocks, 3) }) t.Run("not existing type", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithType(container, "not-exists") + blocks, err = store.GetBlocksWithType(boardID, "not-exists") require.NoError(t, err) require.Len(t, blocks, 0) }) t.Run("valid type", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithType(container, "test") + blocks, err = store.GetBlocksWithType(boardID, "test") require.NoError(t, err) require.Len(t, blocks, 4) }) - t.Run("not existing parent", func(t *testing.T) { + t.Run("not existing board", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithRootID(container, "not-exists") + blocks, err = store.GetBlocksWithBoardID("not-exists") require.NoError(t, err) require.Len(t, blocks, 0) }) - t.Run("valid parent", func(t *testing.T) { + t.Run("all blocks of the a board", func(t *testing.T) { time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksWithRootID(container, "block1") + blocks, err = store.GetBlocksWithBoardID(boardID) require.NoError(t, err) - require.Len(t, blocks, 4) + require.Len(t, blocks, 5) }) } -func testGetBlock(t *testing.T, store store.Store, container store.Container) { +func testGetBlock(t *testing.T, store store.Store) { t.Run("get a block", func(t *testing.T) { block := model.Block{ ID: "block-id-10", - RootID: "root-id-1", + BoardID: "board-id-1", ModifiedBy: "user-id-1", } - err := store.InsertBlock(container, &block, "user-id-1") + err := store.InsertBlock(&block, "user-id-1") require.NoError(t, err) - fetchedBlock, err := store.GetBlock(container, "block-id-10") + fetchedBlock, err := store.GetBlock("block-id-10") require.NoError(t, err) require.NotNil(t, fetchedBlock) require.Equal(t, "block-id-10", fetchedBlock.ID) - require.Equal(t, "root-id-1", fetchedBlock.RootID) + require.Equal(t, "board-id-1", fetchedBlock.BoardID) require.Equal(t, "user-id-1", fetchedBlock.CreatedBy) require.Equal(t, "user-id-1", fetchedBlock.ModifiedBy) assert.WithinDurationf(t, time.Now(), utils.GetTimeForMillis(fetchedBlock.CreateAt), 1*time.Second, "create time should be current time") @@ -866,8 +830,46 @@ func testGetBlock(t *testing.T, store store.Store, container store.Container) { }) t.Run("get a non-existing block", func(t *testing.T) { - fetchedBlock, err := store.GetBlock(container, "non-existing-id") + fetchedBlock, err := store.GetBlock("non-existing-id") require.NoError(t, err) require.Nil(t, fetchedBlock) }) } + +func testDuplicateBlock(t *testing.T, store store.Store) { + InsertBlocks(t, store, subtreeSampleBlocks, "user-id-1") + time.Sleep(1 * time.Millisecond) + defer DeleteBlocks(t, store, subtreeSampleBlocks, "test") + + t.Run("duplicate existing block as no template", func(t *testing.T) { + blocks, err := store.DuplicateBlock(testBoardID, "child1", testUserID, false) + require.NoError(t, err) + require.Len(t, blocks, 3) + require.Equal(t, false, blocks[0].Fields["isTemplate"]) + }) + + t.Run("duplicate existing block as template", func(t *testing.T) { + blocks, err := store.DuplicateBlock(testBoardID, "child1", testUserID, true) + require.NoError(t, err) + require.Len(t, blocks, 3) + require.Equal(t, true, blocks[0].Fields["isTemplate"]) + }) + + t.Run("duplicate not existing block", func(t *testing.T) { + blocks, err := store.DuplicateBlock(testBoardID, "not-existing-id", testUserID, false) + require.Error(t, err) + require.Nil(t, blocks) + }) + + t.Run("duplicate not existing board", func(t *testing.T) { + blocks, err := store.DuplicateBlock("not-existing-board", "not-existing-id", testUserID, false) + require.Error(t, err) + require.Nil(t, blocks) + }) + + t.Run("not matching board/block", func(t *testing.T) { + blocks, err := store.DuplicateBlock("other-id", "child1", testUserID, false) + require.Error(t, err) + require.Nil(t, blocks) + }) +} diff --git a/server/services/store/storetests/boards.go b/server/services/store/storetests/boards.go new file mode 100644 index 000000000..e5c4c172d --- /dev/null +++ b/server/services/store/storetests/boards.go @@ -0,0 +1,770 @@ +package storetests + +import ( + "database/sql" + "testing" + "time" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/store" + "github.com/mattermost/focalboard/server/utils" + + "github.com/stretchr/testify/require" +) + +//nolint:dupl +func StoreTestBoardStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { + t.Run("GetBoard", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testGetBoard(t, store) + }) + t.Run("GetBoardsForUserAndTeam", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testGetBoardsForUserAndTeam(t, store) + }) + t.Run("InsertBoard", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testInsertBoard(t, store) + }) + t.Run("PatchBoard", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testPatchBoard(t, store) + }) + t.Run("DeleteBoard", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testDeleteBoard(t, store) + }) + t.Run("InsertBoardWithAdmin", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testInsertBoardWithAdmin(t, store) + }) + t.Run("SaveMember", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testSaveMember(t, store) + }) + t.Run("GetMemberForBoard", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testGetMemberForBoard(t, store) + }) + t.Run("GetMembersForBoard", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testGetMembersForBoard(t, store) + }) + t.Run("DeleteMember", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testDeleteMember(t, store) + }) + t.Run("SearchBoardsForUserAndTeam", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testSearchBoardsForUserAndTeam(t, store) + }) +} + +func testGetBoard(t *testing.T, store store.Store) { + userID := testUserID + + t.Run("existing board", func(t *testing.T) { + board := &model.Board{ + ID: "id-1", + TeamID: testTeamID, + Type: model.BoardTypeOpen, + } + + _, err := store.InsertBoard(board, userID) + require.NoError(t, err) + + rBoard, err := store.GetBoard(board.ID) + require.NoError(t, err) + require.Equal(t, board.ID, rBoard.ID) + require.Equal(t, board.TeamID, rBoard.TeamID) + require.Equal(t, userID, rBoard.CreatedBy) + require.Equal(t, userID, rBoard.ModifiedBy) + require.Equal(t, board.Type, rBoard.Type) + require.NotZero(t, rBoard.CreateAt) + require.NotZero(t, rBoard.UpdateAt) + }) + + t.Run("nonexisting board", func(t *testing.T) { + rBoard, err := store.GetBoard("nonexistent-id") + require.ErrorIs(t, err, sql.ErrNoRows) + require.Nil(t, rBoard) + }) +} + +func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) { + userID := "user-id-1" + + t.Run("should return only the boards of the team that the user is a member of", func(t *testing.T) { + teamID1 := "team-id-1" + teamID2 := "team-id-2" + + // team 1 boards + board1 := &model.Board{ + ID: "board-id-1", + TeamID: teamID1, + Type: model.BoardTypeOpen, + } + _, _, err := store.InsertBoardWithAdmin(board1, userID) + require.NoError(t, err) + + board2 := &model.Board{ + ID: "board-id-2", + TeamID: teamID1, + Type: model.BoardTypePrivate, + } + _, _, err = store.InsertBoardWithAdmin(board2, userID) + require.NoError(t, err) + + board3 := &model.Board{ + ID: "board-id-3", + TeamID: teamID1, + Type: model.BoardTypeOpen, + } + _, err = store.InsertBoard(board3, "other-user") + require.NoError(t, err) + + board4 := &model.Board{ + ID: "board-id-4", + TeamID: teamID1, + Type: model.BoardTypePrivate, + } + _, err = store.InsertBoard(board4, "other-user") + require.NoError(t, err) + + // team 2 boards + board5 := &model.Board{ + ID: "board-id-5", + TeamID: teamID2, + Type: model.BoardTypeOpen, + } + _, _, err = store.InsertBoardWithAdmin(board5, userID) + require.NoError(t, err) + + board6 := &model.Board{ + ID: "board-id-6", + TeamID: teamID1, + Type: model.BoardTypePrivate, + } + _, err = store.InsertBoard(board6, "other-user") + require.NoError(t, err) + + t.Run("should only find the two boards that the user is a member of for team 1", func(t *testing.T) { + boards, err := store.GetBoardsForUserAndTeam(userID, teamID1) + require.NoError(t, err) + require.Len(t, boards, 2) + + boardIDs := []string{} + for _, board := range boards { + boardIDs = append(boardIDs, board.ID) + } + require.ElementsMatch(t, []string{board1.ID, board2.ID}, boardIDs) + }) + + t.Run("should only find the board that the user is a member of for team 2", func(t *testing.T) { + boards, err := store.GetBoardsForUserAndTeam(userID, teamID2) + require.NoError(t, err) + require.Len(t, boards, 1) + require.Equal(t, board5.ID, boards[0].ID) + }) + }) +} + +func testInsertBoard(t *testing.T, store store.Store) { + userID := testUserID + + t.Run("valid public board", func(t *testing.T) { + board := &model.Board{ + ID: "id-test-public", + TeamID: testTeamID, + Type: model.BoardTypeOpen, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.Equal(t, board.ID, newBoard.ID) + require.Equal(t, newBoard.Type, model.BoardTypeOpen) + require.NotZero(t, newBoard.CreateAt) + require.NotZero(t, newBoard.UpdateAt) + require.Zero(t, newBoard.DeleteAt) + require.Equal(t, userID, newBoard.CreatedBy) + require.Equal(t, newBoard.CreatedBy, newBoard.ModifiedBy) + }) + + t.Run("valid private board", func(t *testing.T) { + board := &model.Board{ + ID: "id-test-private", + TeamID: testTeamID, + Type: model.BoardTypePrivate, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.Equal(t, board.ID, newBoard.ID) + require.Equal(t, newBoard.Type, model.BoardTypePrivate) + require.NotZero(t, newBoard.CreateAt) + require.NotZero(t, newBoard.UpdateAt) + require.Zero(t, newBoard.DeleteAt) + require.Equal(t, userID, newBoard.CreatedBy) + require.Equal(t, newBoard.CreatedBy, newBoard.ModifiedBy) + }) + + t.Run("invalid properties field board", func(t *testing.T) { + board := &model.Board{ + ID: "id-test-props", + TeamID: testTeamID, + Properties: map[string]interface{}{"no-serializable-value": t.Run}, + } + + _, err := store.InsertBoard(board, userID) + require.Error(t, err) + + rBoard, err := store.GetBoard(board.ID) + require.ErrorIs(t, err, sql.ErrNoRows) + require.Nil(t, rBoard) + }) + + t.Run("update board", func(t *testing.T) { + board := &model.Board{ + ID: "id-test-public", + TeamID: testTeamID, + Title: "New title", + } + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + newBoard, err := store.InsertBoard(board, "user2") + require.NoError(t, err) + require.Equal(t, "New title", newBoard.Title) + require.Equal(t, "user2", newBoard.ModifiedBy) + }) + + t.Run("test update board type", func(t *testing.T) { + board := &model.Board{ + ID: "id-test-type-board", + Title: "Public board", + Type: model.BoardTypeOpen, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.Equal(t, model.BoardTypeOpen, newBoard.Type) + + boardUpdate := &model.Board{ + ID: "id-test-type-board", + Type: model.BoardTypePrivate, + } + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + modifiedBoard, err := store.InsertBoard(boardUpdate, userID) + require.NoError(t, err) + require.Equal(t, model.BoardTypePrivate, modifiedBoard.Type) + }) +} + +func testPatchBoard(t *testing.T, store store.Store) { + userID := testUserID + + t.Run("should return error if the board doesn't exist", func(t *testing.T) { + newTitle := "A new title" + patch := &model.BoardPatch{Title: &newTitle} + + board, err := store.PatchBoard("nonexistent-board-id", patch, userID) + require.Error(t, err) + require.Nil(t, board) + }) + + t.Run("should correctly apply a simple patch", func(t *testing.T) { + boardID := utils.NewID(utils.IDTypeBoard) + userID2 := "user-id-2" + + board := &model.Board{ + ID: boardID, + TeamID: testTeamID, + Type: model.BoardTypeOpen, + Title: "A simple title", + Description: "A simple description", + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.NotNil(t, newBoard) + require.Equal(t, userID, newBoard.CreatedBy) + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + newTitle := "A new title" + newDescription := "A new description" + patch := &model.BoardPatch{Title: &newTitle, Description: &newDescription} + patchedBoard, err := store.PatchBoard(boardID, patch, userID2) + require.NoError(t, err) + require.Equal(t, newTitle, patchedBoard.Title) + require.Equal(t, newDescription, patchedBoard.Description) + require.Equal(t, userID, patchedBoard.CreatedBy) + require.Equal(t, userID2, patchedBoard.ModifiedBy) + }) + + t.Run("should correctly update the board properties", func(t *testing.T) { + boardID := utils.NewID(utils.IDTypeBoard) + + board := &model.Board{ + ID: boardID, + TeamID: testTeamID, + Type: model.BoardTypeOpen, + Properties: map[string]interface{}{ + "one": "1", + "two": "2", + }, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.NotNil(t, newBoard) + require.Equal(t, "1", newBoard.Properties["one"].(string)) + require.Equal(t, "2", newBoard.Properties["two"].(string)) + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + patch := &model.BoardPatch{ + UpdatedProperties: map[string]interface{}{"three": "3"}, + DeletedProperties: []string{"one"}, + } + patchedBoard, err := store.PatchBoard(boardID, patch, userID) + require.NoError(t, err) + require.NotContains(t, patchedBoard.Properties, "one") + require.Equal(t, "2", patchedBoard.Properties["two"].(string)) + require.Equal(t, "3", patchedBoard.Properties["three"].(string)) + }) + + t.Run("should correctly modify the board's type", func(t *testing.T) { + boardID := utils.NewID(utils.IDTypeBoard) + + board := &model.Board{ + ID: boardID, + TeamID: testTeamID, + Type: model.BoardTypeOpen, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.NotNil(t, newBoard) + require.Equal(t, newBoard.Type, model.BoardTypeOpen) + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + newType := model.BoardTypePrivate + patch := &model.BoardPatch{Type: &newType} + patchedBoard, err := store.PatchBoard(boardID, patch, userID) + require.NoError(t, err) + require.Equal(t, model.BoardTypePrivate, patchedBoard.Type) + }) + + t.Run("a patch that doesn't include any of the properties should not modify them", func(t *testing.T) { + boardID := utils.NewID(utils.IDTypeBoard) + properties := map[string]interface{}{"prop1": "val1"} + cardProperties := []map[string]interface{}{{"prop2": "val2"}} + columnCalculations := map[string]interface{}{"calc3": "val3"} + + board := &model.Board{ + ID: boardID, + TeamID: testTeamID, + Type: model.BoardTypeOpen, + Properties: properties, + CardProperties: cardProperties, + ColumnCalculations: columnCalculations, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.NotNil(t, newBoard) + require.Equal(t, newBoard.Type, model.BoardTypeOpen) + require.Equal(t, properties, newBoard.Properties) + require.Equal(t, cardProperties, newBoard.CardProperties) + require.Equal(t, columnCalculations, newBoard.ColumnCalculations) + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + newType := model.BoardTypePrivate + patch := &model.BoardPatch{Type: &newType} + patchedBoard, err := store.PatchBoard(boardID, patch, userID) + require.NoError(t, err) + require.Equal(t, model.BoardTypePrivate, patchedBoard.Type) + require.Equal(t, properties, patchedBoard.Properties) + require.Equal(t, cardProperties, patchedBoard.CardProperties) + require.Equal(t, columnCalculations, patchedBoard.ColumnCalculations) + }) + + t.Run("a patch that removes a card property and updates another should work correctly", func(t *testing.T) { + boardID := utils.NewID(utils.IDTypeBoard) + prop1 := map[string]interface{}{"id": "prop1", "value": "val1"} + prop2 := map[string]interface{}{"id": "prop2", "value": "val2"} + prop3 := map[string]interface{}{"id": "prop3", "value": "val3"} + cardProperties := []map[string]interface{}{prop1, prop2, prop3} + + board := &model.Board{ + ID: boardID, + TeamID: testTeamID, + Type: model.BoardTypeOpen, + CardProperties: cardProperties, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.NotNil(t, newBoard) + require.Equal(t, newBoard.Type, model.BoardTypeOpen) + require.Equal(t, cardProperties, newBoard.CardProperties) + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + newProp1 := map[string]interface{}{"id": "prop1", "value": "newval1"} + expectedCardProperties := []map[string]interface{}{newProp1, prop3} + patch := &model.BoardPatch{ + UpdatedCardProperties: []map[string]interface{}{newProp1}, + DeletedCardProperties: []string{"prop2"}, + } + patchedBoard, err := store.PatchBoard(boardID, patch, userID) + require.NoError(t, err) + require.ElementsMatch(t, expectedCardProperties, patchedBoard.CardProperties) + }) +} + +func testDeleteBoard(t *testing.T, store store.Store) { + userID := testUserID + + t.Run("should return an error if the board doesn't exist", func(t *testing.T) { + require.Error(t, store.DeleteBoard("nonexistent-board-id", userID)) + }) + + t.Run("should correctly delete the board", func(t *testing.T) { + boardID := utils.NewID(utils.IDTypeBoard) + + board := &model.Board{ + ID: boardID, + TeamID: testTeamID, + Type: model.BoardTypeOpen, + } + + newBoard, err := store.InsertBoard(board, userID) + require.NoError(t, err) + require.NotNil(t, newBoard) + + rBoard, err := store.GetBoard(boardID) + require.NoError(t, err) + require.NotNil(t, rBoard) + + // wait to avoid hitting pk uniqueness constraint in history + time.Sleep(10 * time.Millisecond) + + require.NoError(t, store.DeleteBoard(boardID, userID)) + + r2Board, err := store.GetBoard(boardID) + require.ErrorIs(t, err, sql.ErrNoRows) + require.Nil(t, r2Board) + }) +} + +func testInsertBoardWithAdmin(t *testing.T, store store.Store) { + userID := testUserID + + t.Run("should correctly create a board and the admin membership with the creator", func(t *testing.T) { + boardID := utils.NewID(utils.IDTypeBoard) + + board := &model.Board{ + ID: boardID, + TeamID: testTeamID, + Type: model.BoardTypeOpen, + } + + newBoard, newMember, err := store.InsertBoardWithAdmin(board, userID) + require.NoError(t, err) + require.NotNil(t, newBoard) + require.Equal(t, userID, newBoard.CreatedBy) + require.Equal(t, userID, newBoard.ModifiedBy) + require.NotNil(t, newMember) + require.Equal(t, userID, newMember.UserID) + require.Equal(t, boardID, newMember.BoardID) + require.True(t, newMember.SchemeAdmin) + require.True(t, newMember.SchemeEditor) + }) +} + +func testSaveMember(t *testing.T, store store.Store) { + userID := testUserID + boardID := testBoardID + + t.Run("should correctly create a member", func(t *testing.T) { + bm := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeAdmin: true, + } + + nbm, err := store.SaveMember(bm) + require.NoError(t, err) + require.Equal(t, userID, nbm.UserID) + require.Equal(t, boardID, nbm.BoardID) + + require.True(t, nbm.SchemeAdmin) + }) + + t.Run("should correctly update a member", func(t *testing.T) { + bm := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeEditor: true, + SchemeViewer: true, + } + + nbm, err := store.SaveMember(bm) + require.NoError(t, err) + require.Equal(t, userID, nbm.UserID) + require.Equal(t, boardID, nbm.BoardID) + + require.False(t, nbm.SchemeAdmin) + require.True(t, nbm.SchemeEditor) + require.True(t, nbm.SchemeViewer) + }) +} + +func testGetMemberForBoard(t *testing.T, store store.Store) { + userID := testUserID + boardID := testBoardID + + t.Run("should return a no rows error for nonexisting membership", func(t *testing.T) { + bm, err := store.GetMemberForBoard(boardID, userID) + require.ErrorIs(t, err, sql.ErrNoRows) + require.Nil(t, bm) + }) + + t.Run("should return the membership if exists", func(t *testing.T) { + bm := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeAdmin: true, + } + + nbm, err := store.SaveMember(bm) + require.NoError(t, err) + require.NotNil(t, nbm) + + rbm, err := store.GetMemberForBoard(boardID, userID) + require.NoError(t, err) + require.NotNil(t, rbm) + require.Equal(t, userID, rbm.UserID) + require.Equal(t, boardID, rbm.BoardID) + require.True(t, rbm.SchemeAdmin) + }) +} + +func testGetMembersForBoard(t *testing.T, store store.Store) { + t.Run("should return empty if there are no members on a board", func(t *testing.T) { + members, err := store.GetMembersForBoard(testBoardID) + require.NoError(t, err) + require.Empty(t, members) + }) + + t.Run("should return the members of the board", func(t *testing.T) { + boardID1 := "board-id-1" + boardID2 := "board-id-2" + + userID1 := "user-id-11" + userID2 := "user-id-12" + userID3 := "user-id-13" + + bm1 := &model.BoardMember{BoardID: boardID1, UserID: userID1, SchemeAdmin: true} + _, err1 := store.SaveMember(bm1) + require.NoError(t, err1) + + bm2 := &model.BoardMember{BoardID: boardID1, UserID: userID2, SchemeEditor: true} + _, err2 := store.SaveMember(bm2) + require.NoError(t, err2) + + bm3 := &model.BoardMember{BoardID: boardID2, UserID: userID3, SchemeAdmin: true} + _, err3 := store.SaveMember(bm3) + require.NoError(t, err3) + + getMemberIDs := func(members []*model.BoardMember) []string { + ids := make([]string, len(members)) + for i, member := range members { + ids[i] = member.UserID + } + return ids + } + + board1Members, err := store.GetMembersForBoard(boardID1) + require.NoError(t, err) + require.Len(t, board1Members, 2) + require.ElementsMatch(t, []string{userID1, userID2}, getMemberIDs(board1Members)) + + board2Members, err := store.GetMembersForBoard(boardID2) + require.NoError(t, err) + require.Len(t, board2Members, 1) + require.ElementsMatch(t, []string{userID3}, getMemberIDs(board2Members)) + }) +} + +func testDeleteMember(t *testing.T, store store.Store) { + userID := testUserID + boardID := testBoardID + + t.Run("should return nil if deleting a nonexistent member", func(t *testing.T) { + require.NoError(t, store.DeleteMember(boardID, userID)) + }) + + t.Run("should correctly delete a member", func(t *testing.T) { + bm := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeAdmin: true, + } + + nbm, err := store.SaveMember(bm) + require.NoError(t, err) + require.NotNil(t, nbm) + + require.NoError(t, store.DeleteMember(boardID, userID)) + + rbm, err := store.GetMemberForBoard(boardID, userID) + require.ErrorIs(t, err, sql.ErrNoRows) + require.Nil(t, rbm) + }) +} + +func testSearchBoardsForUserAndTeam(t *testing.T, store store.Store) { + teamID1 := "team-id-1" + teamID2 := "team-id-2" + userID := "user-id-1" + + t.Run("should return empty if user is not a member of any board and there are no public boards on the team", func(t *testing.T) { + boards, err := store.SearchBoardsForUserAndTeam("", userID, teamID1) + require.NoError(t, err) + require.Empty(t, boards) + }) + + board1 := &model.Board{ + ID: "board-id-1", + TeamID: teamID1, + Type: model.BoardTypeOpen, + Title: "Public Board with admin", + } + _, _, err := store.InsertBoardWithAdmin(board1, userID) + require.NoError(t, err) + + board2 := &model.Board{ + ID: "board-id-2", + TeamID: teamID1, + Type: model.BoardTypeOpen, + Title: "Public Board", + } + _, err = store.InsertBoard(board2, userID) + require.NoError(t, err) + + board3 := &model.Board{ + ID: "board-id-3", + TeamID: teamID1, + Type: model.BoardTypePrivate, + Title: "Private Board with admin", + } + _, _, err = store.InsertBoardWithAdmin(board3, userID) + require.NoError(t, err) + + board4 := &model.Board{ + ID: "board-id-4", + TeamID: teamID1, + Type: model.BoardTypePrivate, + Title: "Private Board", + } + _, err = store.InsertBoard(board4, userID) + require.NoError(t, err) + + board5 := &model.Board{ + ID: "board-id-5", + TeamID: teamID2, + Type: model.BoardTypeOpen, + Title: "Public Board with admin in team 2", + } + _, _, err = store.InsertBoardWithAdmin(board5, userID) + require.NoError(t, err) + + testCases := []struct { + Name string + TeamID string + UserID string + Term string + ExpectedBoardIDs []string + }{ + { + Name: "should find all private boards that the user is a member of and public boards with an empty term", + TeamID: teamID1, + UserID: userID, + Term: "", + ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID}, + }, + { + Name: "should find all with term board", + TeamID: teamID1, + UserID: userID, + Term: "board", + ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID}, + }, + { + Name: "should find only public as per the term, wether user is a member or not", + TeamID: teamID1, + UserID: userID, + Term: "public", + ExpectedBoardIDs: []string{board1.ID, board2.ID}, + }, + { + Name: "should find only private as per the term, wether user is a member or not", + TeamID: teamID1, + UserID: userID, + Term: "priv", + ExpectedBoardIDs: []string{board3.ID}, + }, + { + Name: "should find the only board in team 2", + TeamID: teamID2, + UserID: userID, + Term: "", + ExpectedBoardIDs: []string{board5.ID}, + }, + { + Name: "should find no board in team 2 with a non matching term", + TeamID: teamID2, + UserID: userID, + Term: "non-matching-term", + ExpectedBoardIDs: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + boards, err := store.SearchBoardsForUserAndTeam(tc.Term, tc.UserID, tc.TeamID) + require.NoError(t, err) + + boardIDs := []string{} + for _, board := range boards { + boardIDs = append(boardIDs, board.ID) + } + require.ElementsMatch(t, tc.ExpectedBoardIDs, boardIDs) + }) + } +} diff --git a/server/services/store/storetests/boards_and_blocks.go b/server/services/store/storetests/boards_and_blocks.go new file mode 100644 index 000000000..cd76d77bb --- /dev/null +++ b/server/services/store/storetests/boards_and_blocks.go @@ -0,0 +1,620 @@ +package storetests + +import ( + "database/sql" + "fmt" + "testing" + "time" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/store" + "github.com/mattermost/focalboard/server/utils" + + "github.com/stretchr/testify/require" +) + +func StoreTestBoardsAndBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { + t.Run("createBoardsAndBlocks", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testCreateBoardsAndBlocks(t, store) + }) + t.Run("patchBoardsAndBlocks", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testPatchBoardsAndBlocks(t, store) + }) + t.Run("deleteBoardsAndBlocks", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testDeleteBoardsAndBlocks(t, store) + }) + + t.Run("duplicateBoard", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testDuplicateBoard(t, store) + }) +} + +func testCreateBoardsAndBlocks(t *testing.T, store store.Store) { + teamID := testTeamID + userID := testUserID + + boards, err := store.GetBoardsForUserAndTeam(userID, teamID) + require.Nil(t, err) + require.Empty(t, boards) + + t.Run("create boards and blocks", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-1", TeamID: teamID, Type: model.BoardTypeOpen}, + {ID: "board-id-2", TeamID: teamID, Type: model.BoardTypePrivate}, + {ID: "board-id-3", TeamID: teamID, Type: model.BoardTypeOpen}, + }, + Blocks: []model.Block{ + {ID: "block-id-1", BoardID: "board-id-1", Type: model.TypeCard}, + {ID: "block-id-2", BoardID: "board-id-2", Type: model.TypeCard}, + }, + } + + bab, err := store.CreateBoardsAndBlocks(newBab, userID) + require.Nil(t, err) + require.NotNil(t, bab) + require.Len(t, bab.Boards, 3) + require.Len(t, bab.Blocks, 2) + + boardIDs := []string{} + for _, board := range bab.Boards { + boardIDs = append(boardIDs, board.ID) + } + + blockIDs := []string{} + for _, block := range bab.Blocks { + blockIDs = append(blockIDs, block.ID) + } + + require.ElementsMatch(t, []string{"board-id-1", "board-id-2", "board-id-3"}, boardIDs) + require.ElementsMatch(t, []string{"block-id-1", "block-id-2"}, blockIDs) + }) + + t.Run("create boards and blocks with admin", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-4", TeamID: teamID, Type: model.BoardTypeOpen}, + {ID: "board-id-5", TeamID: teamID, Type: model.BoardTypePrivate}, + {ID: "board-id-6", TeamID: teamID, Type: model.BoardTypeOpen}, + }, + Blocks: []model.Block{ + {ID: "block-id-3", BoardID: "board-id-4", Type: model.TypeCard}, + {ID: "block-id-4", BoardID: "board-id-5", Type: model.TypeCard}, + }, + } + + bab, members, err := store.CreateBoardsAndBlocksWithAdmin(newBab, userID) + require.Nil(t, err) + require.NotNil(t, bab) + require.Len(t, bab.Boards, 3) + require.Len(t, bab.Blocks, 2) + require.Len(t, members, 3) + + boardIDs := []string{} + for _, board := range bab.Boards { + boardIDs = append(boardIDs, board.ID) + } + + blockIDs := []string{} + for _, block := range bab.Blocks { + blockIDs = append(blockIDs, block.ID) + } + + require.ElementsMatch(t, []string{"board-id-4", "board-id-5", "board-id-6"}, boardIDs) + require.ElementsMatch(t, []string{"block-id-3", "block-id-4"}, blockIDs) + + memberBoardIDs := []string{} + for _, member := range members { + require.Equal(t, userID, member.UserID) + memberBoardIDs = append(memberBoardIDs, member.BoardID) + } + require.ElementsMatch(t, []string{"board-id-4", "board-id-5", "board-id-6"}, memberBoardIDs) + }) + + t.Run("on failure, nothing should be saved", func(t *testing.T) { + // one of the blocks is invalid as it doesn't have BoardID + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-7", TeamID: teamID, Type: model.BoardTypeOpen}, + {ID: "board-id-8", TeamID: teamID, Type: model.BoardTypePrivate}, + {ID: "board-id-9", TeamID: teamID, Type: model.BoardTypeOpen}, + }, + Blocks: []model.Block{ + {ID: "block-id-5", BoardID: "board-id-7", Type: model.TypeCard}, + {ID: "block-id-6", BoardID: "", Type: model.TypeCard}, + }, + } + + bab, err := store.CreateBoardsAndBlocks(newBab, userID) + require.Error(t, err) + require.Nil(t, bab) + + bab, members, err := store.CreateBoardsAndBlocksWithAdmin(newBab, userID) + require.Error(t, err) + require.Empty(t, bab) + require.Empty(t, members) + }) +} + +func testPatchBoardsAndBlocks(t *testing.T, store store.Store) { + teamID := testTeamID + userID := testUserID + + t.Run("on failure, nothing should be saved", func(t *testing.T) { + if store.DBType() == model.SqliteDBType { + t.Skip("No transactions support int sqlite") + } + + initialTitle := "initial title" + newTitle := "new title" + + board := &model.Board{ + ID: "board-id-1", + Title: initialTitle, + TeamID: teamID, + Type: model.BoardTypeOpen, + } + _, err := store.InsertBoard(board, userID) + require.NoError(t, err) + + block := model.Block{ + ID: "block-id-1", + BoardID: "board-id-1", + Title: initialTitle, + } + require.NoError(t, store.InsertBlock(&block, userID)) + + // apply the patches + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{"board-id-1"}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle}, + }, + BlockIDs: []string{"block-id-1", "block-id-2"}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Title: &newTitle}, + }, + } + + time.Sleep(10 * time.Millisecond) + + bab, err := store.PatchBoardsAndBlocks(pbab, userID) + require.Error(t, err) + require.Nil(t, bab) + + // check that things have changed + rBoard, err := store.GetBoard("board-id-1") + require.NoError(t, err) + require.Equal(t, initialTitle, rBoard.Title) + + rBlock, err := store.GetBlock("block-id-1") + require.NoError(t, err) + require.Equal(t, initialTitle, rBlock.Title) + }) + + t.Run("patch boards and blocks", func(t *testing.T) { + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-1", Description: "initial description", TeamID: teamID, Type: model.BoardTypeOpen}, + {ID: "board-id-2", TeamID: teamID, Type: model.BoardTypePrivate}, + {ID: "board-id-3", Title: "initial title", TeamID: teamID, Type: model.BoardTypeOpen}, + }, + Blocks: []model.Block{ + {ID: "block-id-1", Title: "initial title", BoardID: "board-id-1", Type: model.TypeCard}, + {ID: "block-id-2", Schema: 1, BoardID: "board-id-2", Type: model.TypeCard}, + }, + } + + rBab, err := store.CreateBoardsAndBlocks(newBab, userID) + require.Nil(t, err) + require.NotNil(t, rBab) + require.Len(t, rBab.Boards, 3) + require.Len(t, rBab.Blocks, 2) + + // apply the patches + newTitle := "new title" + newDescription := "new description" + var newSchema int64 = 2 + + pbab := &model.PatchBoardsAndBlocks{ + BoardIDs: []string{"board-id-3", "board-id-1"}, + BoardPatches: []*model.BoardPatch{ + {Title: &newTitle, Description: &newDescription}, + {Description: &newDescription}, + }, + BlockIDs: []string{"block-id-1", "block-id-2"}, + BlockPatches: []*model.BlockPatch{ + {Title: &newTitle}, + {Schema: &newSchema}, + }, + } + + time.Sleep(10 * time.Millisecond) + + bab, err := store.PatchBoardsAndBlocks(pbab, userID) + require.NoError(t, err) + require.NotNil(t, bab) + require.Len(t, bab.Boards, 2) + require.Len(t, bab.Blocks, 2) + + // check that things have changed + board1, err := store.GetBoard("board-id-1") + require.NoError(t, err) + require.Equal(t, newDescription, board1.Description) + + board3, err := store.GetBoard("board-id-3") + require.NoError(t, err) + require.Equal(t, newTitle, board3.Title) + require.Equal(t, newDescription, board3.Description) + + block1, err := store.GetBlock("block-id-1") + require.NoError(t, err) + require.Equal(t, newTitle, block1.Title) + + block2, err := store.GetBlock("block-id-2") + require.NoError(t, err) + require.Equal(t, newSchema, block2.Schema) + }) +} + +func testDeleteBoardsAndBlocks(t *testing.T, store store.Store) { + teamID := testTeamID + userID := testUserID + + t.Run("should not delete anything if a block doesn't belong to any of the boards", func(t *testing.T) { + if store.DBType() == model.SqliteDBType { + t.Skip("No transactions support int sqlite") + } + + newBoard1 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := store.InsertBoard(newBoard1, userID) + require.NoError(t, err) + + block1 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block1, userID)) + + block2 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block2, userID)) + + newBoard2 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := store.InsertBoard(newBoard2, userID) + require.NoError(t, err) + + block3 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board2.ID, + } + require.NoError(t, store.InsertBlock(block3, userID)) + + block4 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: "different-board-id", + } + require.NoError(t, store.InsertBlock(block4, userID)) + + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{board1.ID, board2.ID}, + Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID}, + } + + time.Sleep(10 * time.Millisecond) + + expectedErrorMsg := fmt.Sprintf("block %s doesn't belong to any of the boards in the delete request", block4.ID) + require.EqualError(t, store.DeleteBoardsAndBlocks(dbab, userID), expectedErrorMsg) + + // all the entities should still exist + rBoard1, err := store.GetBoard(board1.ID) + require.NoError(t, err) + require.NotNil(t, rBoard1) + rBlock1, err := store.GetBlock(block1.ID) + require.NoError(t, err) + require.NotNil(t, rBlock1) + rBlock2, err := store.GetBlock(block2.ID) + require.NoError(t, err) + require.NotNil(t, rBlock2) + + rBoard2, err := store.GetBoard(board2.ID) + require.NoError(t, err) + require.NotNil(t, rBoard2) + rBlock3, err := store.GetBlock(block3.ID) + require.NoError(t, err) + require.NotNil(t, rBlock3) + rBlock4, err := store.GetBlock(block4.ID) + require.NoError(t, err) + require.NotNil(t, rBlock4) + }) + + t.Run("should not delete anything if a board doesn't exist", func(t *testing.T) { + if store.DBType() == model.SqliteDBType { + t.Skip("No transactions support int sqlite") + } + + newBoard1 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := store.InsertBoard(newBoard1, userID) + require.NoError(t, err) + + block1 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block1, userID)) + + block2 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block2, userID)) + + newBoard2 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := store.InsertBoard(newBoard2, userID) + require.NoError(t, err) + + block3 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board2.ID, + } + require.NoError(t, store.InsertBlock(block3, userID)) + + block4 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board2.ID, + } + require.NoError(t, store.InsertBlock(block4, userID)) + + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{board1.ID, board2.ID, "a nonexistent board ID"}, + Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID}, + } + + time.Sleep(10 * time.Millisecond) + + require.ErrorIs(t, store.DeleteBoardsAndBlocks(dbab, userID), sql.ErrNoRows) + + // all the entities should still exist + rBoard1, err := store.GetBoard(board1.ID) + require.NoError(t, err) + require.NotNil(t, rBoard1) + rBlock1, err := store.GetBlock(block1.ID) + require.NoError(t, err) + require.NotNil(t, rBlock1) + rBlock2, err := store.GetBlock(block2.ID) + require.NoError(t, err) + require.NotNil(t, rBlock2) + + rBoard2, err := store.GetBoard(board2.ID) + require.NoError(t, err) + require.NotNil(t, rBoard2) + rBlock3, err := store.GetBlock(block3.ID) + require.NoError(t, err) + require.NotNil(t, rBlock3) + rBlock4, err := store.GetBlock(block4.ID) + require.NoError(t, err) + require.NotNil(t, rBlock4) + }) + + t.Run("should not delete anything if a block doesn't exist", func(t *testing.T) { + if store.DBType() == model.SqliteDBType { + t.Skip("No transactions support int sqlite") + } + + newBoard1 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := store.InsertBoard(newBoard1, userID) + require.NoError(t, err) + + block1 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block1, userID)) + + block2 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block2, userID)) + + newBoard2 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := store.InsertBoard(newBoard2, userID) + require.NoError(t, err) + + block3 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board2.ID, + } + require.NoError(t, store.InsertBlock(block3, userID)) + + block4 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board2.ID, + } + require.NoError(t, store.InsertBlock(block4, userID)) + + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{board1.ID, board2.ID}, + Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID, "a nonexistent block ID"}, + } + + time.Sleep(10 * time.Millisecond) + + require.ErrorIs(t, store.DeleteBoardsAndBlocks(dbab, userID), sql.ErrNoRows) + + // all the entities should still exist + rBoard1, err := store.GetBoard(board1.ID) + require.NoError(t, err) + require.NotNil(t, rBoard1) + rBlock1, err := store.GetBlock(block1.ID) + require.NoError(t, err) + require.NotNil(t, rBlock1) + rBlock2, err := store.GetBlock(block2.ID) + require.NoError(t, err) + require.NotNil(t, rBlock2) + + rBoard2, err := store.GetBoard(board2.ID) + require.NoError(t, err) + require.NotNil(t, rBoard2) + rBlock3, err := store.GetBlock(block3.ID) + require.NoError(t, err) + require.NotNil(t, rBlock3) + rBlock4, err := store.GetBlock(block4.ID) + require.NoError(t, err) + require.NotNil(t, rBlock4) + }) + + t.Run("should not work properly if all the entities are related", func(t *testing.T) { + newBoard1 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board1, err := store.InsertBoard(newBoard1, userID) + require.NoError(t, err) + + block1 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block1, userID)) + + block2 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board1.ID, + } + require.NoError(t, store.InsertBlock(block2, userID)) + + newBoard2 := &model.Board{ + ID: utils.NewID(utils.IDTypeBoard), + TeamID: teamID, + Type: model.BoardTypeOpen, + } + board2, err := store.InsertBoard(newBoard2, userID) + require.NoError(t, err) + + block3 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board2.ID, + } + require.NoError(t, store.InsertBlock(block3, userID)) + + block4 := &model.Block{ + ID: utils.NewID(utils.IDTypeBlock), + BoardID: board2.ID, + } + require.NoError(t, store.InsertBlock(block4, userID)) + + dbab := &model.DeleteBoardsAndBlocks{ + Boards: []string{board1.ID, board2.ID}, + Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID}, + } + + time.Sleep(10 * time.Millisecond) + + require.NoError(t, store.DeleteBoardsAndBlocks(dbab, userID)) + + rBoard1, err := store.GetBoard(board1.ID) + require.Error(t, err) + require.Nil(t, rBoard1) + rBlock1, err := store.GetBlock(block1.ID) + require.NoError(t, err) + require.Nil(t, rBlock1) + rBlock2, err := store.GetBlock(block2.ID) + require.NoError(t, err) + require.Nil(t, rBlock2) + + rBoard2, err := store.GetBoard(board2.ID) + require.Error(t, err) + require.Nil(t, rBoard2) + rBlock3, err := store.GetBlock(block3.ID) + require.NoError(t, err) + require.Nil(t, rBlock3) + rBlock4, err := store.GetBlock(block4.ID) + require.NoError(t, err) + require.Nil(t, rBlock4) + }) +} + +func testDuplicateBoard(t *testing.T, store store.Store) { + teamID := testTeamID + userID := testUserID + + newBab := &model.BoardsAndBlocks{ + Boards: []*model.Board{ + {ID: "board-id-1", TeamID: teamID, Type: model.BoardTypeOpen}, + {ID: "board-id-2", TeamID: teamID, Type: model.BoardTypePrivate}, + {ID: "board-id-3", TeamID: teamID, Type: model.BoardTypeOpen}, + }, + Blocks: []model.Block{ + {ID: "block-id-1", BoardID: "board-id-1", Type: model.TypeCard}, + {ID: "block-id-2", BoardID: "board-id-2", Type: model.TypeCard}, + }, + } + + bab, err := store.CreateBoardsAndBlocks(newBab, userID) + require.Nil(t, err) + require.NotNil(t, bab) + require.Len(t, bab.Boards, 3) + require.Len(t, bab.Blocks, 2) + + t.Run("duplicate existing board as no template", func(t *testing.T) { + bab, members, err := store.DuplicateBoard("board-id-1", userID, teamID, false) + require.NoError(t, err) + require.Len(t, members, 1) + require.Len(t, bab.Boards, 1) + require.Len(t, bab.Blocks, 1) + require.Equal(t, bab.Boards[0].IsTemplate, false) + }) + + t.Run("duplicate existing board as template", func(t *testing.T) { + bab, members, err := store.DuplicateBoard("board-id-1", userID, teamID, true) + require.NoError(t, err) + require.Len(t, members, 1) + require.Len(t, bab.Boards, 1) + require.Len(t, bab.Blocks, 1) + require.Equal(t, bab.Boards[0].IsTemplate, true) + }) + + t.Run("duplicate not existing board", func(t *testing.T) { + bab, members, err := store.DuplicateBoard("not-existing-id", userID, teamID, false) + require.Error(t, err) + require.Nil(t, members) + require.Nil(t, bab) + }) +} diff --git a/server/services/store/storetests/helpers.go b/server/services/store/storetests/helpers.go index 371ec036f..dc9a2db90 100644 --- a/server/services/store/storetests/helpers.go +++ b/server/services/store/storetests/helpers.go @@ -8,16 +8,16 @@ import ( "github.com/stretchr/testify/require" ) -func InsertBlocks(t *testing.T, s store.Store, container store.Container, blocks []model.Block, userID string) { +func InsertBlocks(t *testing.T, s store.Store, blocks []model.Block, userID string) { for i := range blocks { - err := s.InsertBlock(container, &blocks[i], userID) + err := s.InsertBlock(&blocks[i], userID) require.NoError(t, err) } } -func DeleteBlocks(t *testing.T, s store.Store, container store.Container, blocks []model.Block, modifiedBy string) { +func DeleteBlocks(t *testing.T, s store.Store, blocks []model.Block, modifiedBy string) { for _, block := range blocks { - err := s.DeleteBlock(container, block.ID, modifiedBy) + err := s.DeleteBlock(block.ID, modifiedBy) require.NoError(t, err) } } diff --git a/server/services/store/storetests/notificationhints.go b/server/services/store/storetests/notificationhints.go index 6d2da690b..dbba5dbb3 100644 --- a/server/services/store/storetests/notificationhints.go +++ b/server/services/store/storetests/notificationhints.go @@ -16,42 +16,37 @@ import ( ) func StoreTestNotificationHintsStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { - container := store.Container{ - WorkspaceID: "0", - } - t.Run("UpsertNotificationHint", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testUpsertNotificationHint(t, store, container) + testUpsertNotificationHint(t, store) }) t.Run("DeleteNotificationHint", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testDeleteNotificationHint(t, store, container) + testDeleteNotificationHint(t, store) }) t.Run("GetNotificationHint", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetNotificationHint(t, store, container) + testGetNotificationHint(t, store) }) t.Run("GetNextNotificationHint", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetNextNotificationHint(t, store, container) + testGetNextNotificationHint(t, store) }) } -func testUpsertNotificationHint(t *testing.T, store store.Store, container store.Container) { +func testUpsertNotificationHint(t *testing.T, store store.Store) { t.Run("create notification hint", func(t *testing.T) { hint := &model.NotificationHint{ BlockType: model.TypeCard, BlockID: utils.NewID(utils.IDTypeBlock), ModifiedByID: utils.NewID(utils.IDTypeUser), - WorkspaceID: container.WorkspaceID, } hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) @@ -65,7 +60,6 @@ func testUpsertNotificationHint(t *testing.T, store store.Store, container store BlockType: model.TypeCard, BlockID: utils.NewID(utils.IDTypeBlock), ModifiedByID: utils.NewID(utils.IDTypeUser), - WorkspaceID: container.WorkspaceID, } hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) require.NoError(t, err, "upsert notification hint should not error") @@ -77,7 +71,6 @@ func testUpsertNotificationHint(t *testing.T, store store.Store, container store BlockType: model.TypeCard, BlockID: hintNew.BlockID, ModifiedByID: hintNew.ModifiedByID, - WorkspaceID: container.WorkspaceID, } hintDup, err := store.UpsertNotificationHint(hint, time.Second*15) @@ -92,11 +85,10 @@ func testUpsertNotificationHint(t *testing.T, store store.Store, container store _, err := store.UpsertNotificationHint(hint, time.Second*15) assert.ErrorAs(t, err, &model.ErrInvalidNotificationHint{}, "invalid notification hint should error") - hint.BlockType = model.TypeBoard + hint.BlockType = "board" _, err = store.UpsertNotificationHint(hint, time.Second*15) assert.ErrorAs(t, err, &model.ErrInvalidNotificationHint{}, "invalid notification hint should error") - hint.WorkspaceID = container.WorkspaceID _, err = store.UpsertNotificationHint(hint, time.Second*15) assert.ErrorAs(t, err, &model.ErrInvalidNotificationHint{}, "invalid notification hint should error") @@ -111,63 +103,61 @@ func testUpsertNotificationHint(t *testing.T, store store.Store, container store }) } -func testDeleteNotificationHint(t *testing.T, store store.Store, container store.Container) { +func testDeleteNotificationHint(t *testing.T, store store.Store) { t.Run("delete notification hint", func(t *testing.T) { hint := &model.NotificationHint{ BlockType: model.TypeCard, BlockID: utils.NewID(utils.IDTypeBlock), ModifiedByID: utils.NewID(utils.IDTypeUser), - WorkspaceID: container.WorkspaceID, } hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) require.NoError(t, err, "create notification hint should not error") // check the notification hint exists - hint, err = store.GetNotificationHint(container, hintNew.BlockID) + hint, err = store.GetNotificationHint(hintNew.BlockID) require.NoError(t, err, "get notification hint should not error") assert.Equal(t, hintNew.BlockID, hint.BlockID) assert.Equal(t, hintNew.CreateAt, hint.CreateAt) - err = store.DeleteNotificationHint(container, hintNew.BlockID) + err = store.DeleteNotificationHint(hintNew.BlockID) require.NoError(t, err, "delete notification hint should not error") // check the notification hint was deleted - hint, err = store.GetNotificationHint(container, hintNew.BlockID) + hint, err = store.GetNotificationHint(hintNew.BlockID) require.True(t, store.IsErrNotFound(err), "error should be of type store.ErrNotFound") assert.Nil(t, hint) }) t.Run("delete non-existent notification hint", func(t *testing.T) { - err := store.DeleteNotificationHint(container, "bogus") + err := store.DeleteNotificationHint("bogus") require.True(t, store.IsErrNotFound(err), "error should be of type store.ErrNotFound") }) } -func testGetNotificationHint(t *testing.T, store store.Store, container store.Container) { +func testGetNotificationHint(t *testing.T, store store.Store) { t.Run("get notification hint", func(t *testing.T) { hint := &model.NotificationHint{ BlockType: model.TypeCard, BlockID: utils.NewID(utils.IDTypeBlock), ModifiedByID: utils.NewID(utils.IDTypeUser), - WorkspaceID: container.WorkspaceID, } hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) require.NoError(t, err, "create notification hint should not error") // make sure notification hint can be fetched - hint, err = store.GetNotificationHint(container, hintNew.BlockID) + hint, err = store.GetNotificationHint(hintNew.BlockID) require.NoError(t, err, "get notification hint should not error") assert.Equal(t, hintNew, hint) }) t.Run("get non-existent notification hint", func(t *testing.T) { - hint, err := store.GetNotificationHint(container, "bogus") + hint, err := store.GetNotificationHint("bogus") require.True(t, store.IsErrNotFound(err), "error should be of type store.ErrNotFound") assert.Nil(t, hint, "hint should be nil") }) } -func testGetNextNotificationHint(t *testing.T, store store.Store, container store.Container) { +func testGetNextNotificationHint(t *testing.T, store store.Store) { t.Run("get next notification hint", func(t *testing.T) { const loops = 5 ids := [5]string{} @@ -179,7 +169,6 @@ func testGetNextNotificationHint(t *testing.T, store store.Store, container stor BlockType: model.TypeCard, BlockID: utils.NewID(utils.IDTypeBlock), ModifiedByID: modifiedBy, - WorkspaceID: container.WorkspaceID, } hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) require.NoError(t, err, "create notification hint should not error") @@ -198,7 +187,7 @@ func testGetNextNotificationHint(t *testing.T, store store.Store, container stor assert.Less(t, notifyAt, hint.NotifyAt) notifyAt = hint.NotifyAt - err = store.DeleteNotificationHint(container, hint.BlockID) + err = store.DeleteNotificationHint(hint.BlockID) require.NoError(t, err, "delete notification hint should not error") } }) @@ -215,7 +204,7 @@ func testGetNextNotificationHint(t *testing.T, store store.Store, container stor } require.NoError(t, err2, "get next notification hint should not error") - err2 = store.DeleteNotificationHint(container, hint.BlockID) + err2 = store.DeleteNotificationHint(hint.BlockID) require.NoError(t, err2, "delete notification hint should not error") } @@ -232,7 +221,6 @@ func testGetNextNotificationHint(t *testing.T, store store.Store, container stor BlockType: model.TypeCard, BlockID: utils.NewID(utils.IDTypeBlock), ModifiedByID: utils.NewID(utils.IDTypeUser), - WorkspaceID: container.WorkspaceID, } hintNew, err := store.UpsertNotificationHint(hint, time.Second*1) require.NoError(t, err, "create notification hint should not error") @@ -259,9 +247,7 @@ func emptyNotificationHintTable(store store.Store) error { return err } - c := containerForWorkspace(hint.WorkspaceID) - - err = store.DeleteNotificationHint(c, hint.BlockID) + err = store.DeleteNotificationHint(hint.BlockID) if err != nil { return err } diff --git a/server/services/store/storetests/session.go b/server/services/store/storetests/session.go index b213f0c4e..42ae691a1 100644 --- a/server/services/store/storetests/session.go +++ b/server/services/store/storetests/session.go @@ -11,30 +11,26 @@ import ( ) func StoreTestSessionStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { - container := store.Container{ - WorkspaceID: "0", - } - t.Run("CreateAndGetAndDeleteSession", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testCreateAndGetAndDeleteSession(t, store, container) + testCreateAndGetAndDeleteSession(t, store) }) t.Run("GetActiveUserCount", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetActiveUserCount(t, store, container) + testGetActiveUserCount(t, store) }) t.Run("UpdateSession", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testUpdateSession(t, store, container) + testUpdateSession(t, store) }) } -func testCreateAndGetAndDeleteSession(t *testing.T, store store.Store, _ store.Container) { +func testCreateAndGetAndDeleteSession(t *testing.T, store store.Store) { session := &model.Session{ ID: "session-id", Token: "token", @@ -58,7 +54,7 @@ func testCreateAndGetAndDeleteSession(t *testing.T, store store.Store, _ store.C }) } -func testGetActiveUserCount(t *testing.T, store store.Store, _ store.Container) { +func testGetActiveUserCount(t *testing.T, store store.Store) { t.Run("no active user", func(t *testing.T) { count, err := store.GetActiveUserCount(60) require.NoError(t, err) @@ -84,7 +80,7 @@ func testGetActiveUserCount(t *testing.T, store store.Store, _ store.Container) }) } -func testUpdateSession(t *testing.T, store store.Store, _ store.Container) { +func testUpdateSession(t *testing.T, store store.Store) { session := &model.Session{ ID: "session-id", Token: "token", diff --git a/server/services/store/storetests/sharing.go b/server/services/store/storetests/sharing.go index d139de1d2..7de44ed0e 100644 --- a/server/services/store/storetests/sharing.go +++ b/server/services/store/storetests/sharing.go @@ -9,18 +9,14 @@ import ( ) func StoreTestSharingStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { - container := store.Container{ - WorkspaceID: "0", - } - t.Run("UpsertSharingAndGetSharing", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testUpsertSharingAndGetSharing(t, store, container) + testUpsertSharingAndGetSharing(t, store) }) } -func testUpsertSharingAndGetSharing(t *testing.T, store store.Store, container store.Container) { +func testUpsertSharingAndGetSharing(t *testing.T, store store.Store) { t.Run("Insert first sharing and get it", func(t *testing.T) { sharing := model.Sharing{ ID: "sharing-id", @@ -29,9 +25,9 @@ func testUpsertSharingAndGetSharing(t *testing.T, store store.Store, container s ModifiedBy: testUserID, } - err := store.UpsertSharing(container, sharing) + err := store.UpsertSharing(sharing) require.NoError(t, err) - newSharing, err := store.GetSharing(container, "sharing-id") + newSharing, err := store.GetSharing("sharing-id") require.NoError(t, err) newSharing.UpdateAt = 0 require.Equal(t, sharing, *newSharing) @@ -44,20 +40,20 @@ func testUpsertSharingAndGetSharing(t *testing.T, store store.Store, container s ModifiedBy: "user-id2", } - newSharing, err := store.GetSharing(container, "sharing-id") + newSharing, err := store.GetSharing("sharing-id") require.NoError(t, err) newSharing.UpdateAt = 0 require.NotEqual(t, sharing, *newSharing) - err = store.UpsertSharing(container, sharing) + err = store.UpsertSharing(sharing) require.NoError(t, err) - newSharing, err = store.GetSharing(container, "sharing-id") + newSharing, err = store.GetSharing("sharing-id") require.NoError(t, err) newSharing.UpdateAt = 0 require.Equal(t, sharing, *newSharing) }) t.Run("Get not existing sharing", func(t *testing.T) { - _, err := store.GetSharing(container, "not-existing") + _, err := store.GetSharing("not-existing") require.Error(t, err) }) } diff --git a/server/services/store/storetests/subscriptions.go b/server/services/store/storetests/subscriptions.go index 3abb2ec93..5511bfbb3 100644 --- a/server/services/store/storetests/subscriptions.go +++ b/server/services/store/storetests/subscriptions.go @@ -14,51 +14,47 @@ import ( ) func StoreTestSubscriptionsStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { - container := store.Container{ - WorkspaceID: "0", - } - t.Run("CreateSubscription", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testCreateSubscription(t, store, container) + testCreateSubscription(t, store) }) t.Run("DeleteSubscription", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testDeleteSubscription(t, store, container) + testDeleteSubscription(t, store) }) t.Run("UndeleteSubscription", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testUndeleteSubscription(t, store, container) + testUndeleteSubscription(t, store) }) t.Run("GetSubscription", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetSubscription(t, store, container) + testGetSubscription(t, store) }) t.Run("GetSubscriptions", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetSubscriptions(t, store, container) + testGetSubscriptions(t, store) }) t.Run("GetSubscribersForBlock", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetSubscribersForBlock(t, store, container) + testGetSubscribersForBlock(t, store) }) } -func testCreateSubscription(t *testing.T, store store.Store, container store.Container) { +func testCreateSubscription(t *testing.T, store store.Store) { t.Run("create subscriptions", func(t *testing.T) { users := createTestUsers(t, store, 10) - blocks := createTestBlocks(t, store, container, users[0].ID, 50) + blocks := createTestBlocks(t, store, users[0].ID, 50) for i, user := range users { for j := 0; j < i; j++ { @@ -68,7 +64,7 @@ func testCreateSubscription(t *testing.T, store store.Store, container store.Con SubscriberType: "user", SubscriberID: user.ID, } - subNew, err := store.CreateSubscription(container, sub) + subNew, err := store.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") assert.NotZero(t, subNew.NotifiedAt) @@ -79,7 +75,7 @@ func testCreateSubscription(t *testing.T, store store.Store, container store.Con // ensure each user has the right number of subscriptions for i, user := range users { - subs, err := store.GetSubscriptions(container, user.ID) + subs, err := store.GetSubscriptions(user.ID) require.NoError(t, err, "get subscriptions should not error") assert.Len(t, subs, i) } @@ -88,7 +84,7 @@ func testCreateSubscription(t *testing.T, store store.Store, container store.Con t.Run("duplicate subscription", func(t *testing.T) { admin := createTestUsers(t, store, 1)[0] user := createTestUsers(t, store, 1)[0] - block := createTestBlocks(t, store, container, admin.ID, 1)[0] + block := createTestBlocks(t, store, admin.ID, 1)[0] sub := &model.Subscription{ BlockType: block.Type, @@ -96,7 +92,7 @@ func testCreateSubscription(t *testing.T, store store.Store, container store.Con SubscriberType: "user", SubscriberID: user.ID, } - subNew, err := store.CreateSubscription(container, sub) + subNew, err := store.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") sub = &model.Subscription{ @@ -106,48 +102,47 @@ func testCreateSubscription(t *testing.T, store store.Store, container store.Con SubscriberID: user.ID, } - subDup, err := store.CreateSubscription(container, sub) + subDup, err := store.CreateSubscription(sub) require.NoError(t, err, "create duplicate subscription should not error") assert.Equal(t, subNew.BlockID, subDup.BlockID) - assert.Equal(t, subNew.WorkspaceID, subDup.WorkspaceID) assert.Equal(t, subNew.SubscriberID, subDup.SubscriberID) }) t.Run("invalid subscription", func(t *testing.T) { admin := createTestUsers(t, store, 1)[0] user := createTestUsers(t, store, 1)[0] - block := createTestBlocks(t, store, container, admin.ID, 1)[0] + block := createTestBlocks(t, store, admin.ID, 1)[0] sub := &model.Subscription{} - _, err := store.CreateSubscription(container, sub) + _, err := store.CreateSubscription(sub) assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") sub.BlockType = block.Type - _, err = store.CreateSubscription(container, sub) + _, err = store.CreateSubscription(sub) assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") sub.BlockID = block.ID - _, err = store.CreateSubscription(container, sub) + _, err = store.CreateSubscription(sub) assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") sub.SubscriberType = "user" - _, err = store.CreateSubscription(container, sub) + _, err = store.CreateSubscription(sub) assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") sub.SubscriberID = user.ID - subNew, err := store.CreateSubscription(container, sub) + subNew, err := store.CreateSubscription(sub) assert.NoError(t, err, "valid subscription should not error") assert.NoError(t, subNew.IsValid(), "created subscription should be valid") }) } -func testDeleteSubscription(t *testing.T, s store.Store, container store.Container) { +func testDeleteSubscription(t *testing.T, s store.Store) { t.Run("delete subscription", func(t *testing.T) { user := createTestUsers(t, s, 1)[0] - block := createTestBlocks(t, s, container, user.ID, 1)[0] + block := createTestBlocks(t, s, user.ID, 1)[0] sub := &model.Subscription{ BlockType: block.Type, @@ -155,27 +150,27 @@ func testDeleteSubscription(t *testing.T, s store.Store, container store.Contain SubscriberType: "user", SubscriberID: user.ID, } - subNew, err := s.CreateSubscription(container, sub) + subNew, err := s.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") // check the subscription exists - subs, err := s.GetSubscriptions(container, user.ID) + subs, err := s.GetSubscriptions(user.ID) require.NoError(t, err, "get subscriptions should not error") assert.Len(t, subs, 1) assert.Equal(t, subNew.BlockID, subs[0].BlockID) assert.Equal(t, subNew.SubscriberID, subs[0].SubscriberID) - err = s.DeleteSubscription(container, block.ID, user.ID) + err = s.DeleteSubscription(block.ID, user.ID) require.NoError(t, err, "delete subscription should not error") // check the subscription was deleted - subs, err = s.GetSubscriptions(container, user.ID) + subs, err = s.GetSubscriptions(user.ID) require.NoError(t, err, "get subscriptions should not error") assert.Empty(t, subs) }) t.Run("delete non-existent subscription", func(t *testing.T) { - err := s.DeleteSubscription(container, "bogus", "bogus") + err := s.DeleteSubscription("bogus", "bogus") require.Error(t, err, "delete non-existent subscription should error") var nf *store.ErrNotFound require.ErrorAs(t, err, &nf, "error should be of type store.ErrNotFound") @@ -183,10 +178,10 @@ func testDeleteSubscription(t *testing.T, s store.Store, container store.Contain }) } -func testUndeleteSubscription(t *testing.T, s store.Store, container store.Container) { +func testUndeleteSubscription(t *testing.T, s store.Store) { t.Run("undelete subscription", func(t *testing.T) { user := createTestUsers(t, s, 1)[0] - block := createTestBlocks(t, s, container, user.ID, 1)[0] + block := createTestBlocks(t, s, user.ID, 1)[0] sub := &model.Subscription{ BlockType: block.Type, @@ -194,30 +189,30 @@ func testUndeleteSubscription(t *testing.T, s store.Store, container store.Conta SubscriberType: "user", SubscriberID: user.ID, } - subNew, err := s.CreateSubscription(container, sub) + subNew, err := s.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") // check the subscription exists - subs, err := s.GetSubscriptions(container, user.ID) + subs, err := s.GetSubscriptions(user.ID) require.NoError(t, err, "get subscriptions should not error") assert.Len(t, subs, 1) assert.Equal(t, subNew.BlockID, subs[0].BlockID) assert.Equal(t, subNew.SubscriberID, subs[0].SubscriberID) - err = s.DeleteSubscription(container, block.ID, user.ID) + err = s.DeleteSubscription(block.ID, user.ID) require.NoError(t, err, "delete subscription should not error") // check the subscription was deleted - subs, err = s.GetSubscriptions(container, user.ID) + subs, err = s.GetSubscriptions(user.ID) require.NoError(t, err, "get subscriptions should not error") assert.Empty(t, subs) // re-create the subscription - subUndeleted, err := s.CreateSubscription(container, sub) + subUndeleted, err := s.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") // check the undeleted subscription exists - subs, err = s.GetSubscriptions(container, user.ID) + subs, err = s.GetSubscriptions(user.ID) require.NoError(t, err, "get subscriptions should not error") assert.Len(t, subs, 1) assert.Equal(t, subUndeleted.BlockID, subs[0].BlockID) @@ -225,10 +220,10 @@ func testUndeleteSubscription(t *testing.T, s store.Store, container store.Conta }) } -func testGetSubscription(t *testing.T, s store.Store, container store.Container) { +func testGetSubscription(t *testing.T, s store.Store) { t.Run("get subscription", func(t *testing.T) { user := createTestUsers(t, s, 1)[0] - block := createTestBlocks(t, s, container, user.ID, 1)[0] + block := createTestBlocks(t, s, user.ID, 1)[0] sub := &model.Subscription{ BlockType: block.Type, @@ -236,17 +231,17 @@ func testGetSubscription(t *testing.T, s store.Store, container store.Container) SubscriberType: "user", SubscriberID: user.ID, } - subNew, err := s.CreateSubscription(container, sub) + subNew, err := s.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") // make sure subscription can be fetched - sub, err = s.GetSubscription(container, block.ID, user.ID) + sub, err = s.GetSubscription(block.ID, user.ID) require.NoError(t, err, "get subscription should not error") assert.Equal(t, subNew, sub) }) t.Run("get non-existent subscription", func(t *testing.T) { - sub, err := s.GetSubscription(container, "bogus", "bogus") + sub, err := s.GetSubscription("bogus", "bogus") require.Error(t, err, "get non-existent subscription should error") var nf *store.ErrNotFound require.ErrorAs(t, err, &nf, "error should be of type store.ErrNotFound") @@ -255,11 +250,11 @@ func testGetSubscription(t *testing.T, s store.Store, container store.Container) }) } -func testGetSubscriptions(t *testing.T, store store.Store, container store.Container) { +func testGetSubscriptions(t *testing.T, store store.Store) { t.Run("get subscriptions", func(t *testing.T) { author := createTestUsers(t, store, 1)[0] user := createTestUsers(t, store, 1)[0] - blocks := createTestBlocks(t, store, container, author.ID, 50) + blocks := createTestBlocks(t, store, author.ID, 50) for _, block := range blocks { sub := &model.Subscription{ @@ -268,32 +263,32 @@ func testGetSubscriptions(t *testing.T, store store.Store, container store.Conta SubscriberType: "user", SubscriberID: user.ID, } - _, err := store.CreateSubscription(container, sub) + _, err := store.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") } // ensure user has the right number of subscriptions - subs, err := store.GetSubscriptions(container, user.ID) + subs, err := store.GetSubscriptions(user.ID) require.NoError(t, err, "get subscriptions should not error") assert.Len(t, subs, len(blocks)) // ensure author has no subscriptions - subs, err = store.GetSubscriptions(container, author.ID) + subs, err = store.GetSubscriptions(author.ID) require.NoError(t, err, "get subscriptions should not error") assert.Empty(t, subs) }) t.Run("get subscriptions for invalid user", func(t *testing.T) { - subs, err := store.GetSubscriptions(container, "bogus") + subs, err := store.GetSubscriptions("bogus") require.NoError(t, err, "get subscriptions should not error") assert.Empty(t, subs) }) } -func testGetSubscribersForBlock(t *testing.T, store store.Store, container store.Container) { +func testGetSubscribersForBlock(t *testing.T, store store.Store) { t.Run("get subscribers for block", func(t *testing.T) { users := createTestUsers(t, store, 50) - blocks := createTestBlocks(t, store, container, users[0].ID, 2) + blocks := createTestBlocks(t, store, users[0].ID, 2) for _, user := range users { sub := &model.Subscription{ @@ -302,31 +297,31 @@ func testGetSubscribersForBlock(t *testing.T, store store.Store, container store SubscriberType: "user", SubscriberID: user.ID, } - _, err := store.CreateSubscription(container, sub) + _, err := store.CreateSubscription(sub) require.NoError(t, err, "create subscription should not error") } // make sure block[1] has the right number of users subscribed - subs, err := store.GetSubscribersForBlock(container, blocks[1].ID) + subs, err := store.GetSubscribersForBlock(blocks[1].ID) require.NoError(t, err, "get subscribers for block should not error") assert.Len(t, subs, 50) - count, err := store.GetSubscribersCountForBlock(container, blocks[1].ID) + count, err := store.GetSubscribersCountForBlock(blocks[1].ID) require.NoError(t, err, "get subscribers for block should not error") assert.Equal(t, 50, count) // make sure block[0] has zero users subscribed - subs, err = store.GetSubscribersForBlock(container, blocks[0].ID) + subs, err = store.GetSubscribersForBlock(blocks[0].ID) require.NoError(t, err, "get subscribers for block should not error") assert.Empty(t, subs) - count, err = store.GetSubscribersCountForBlock(container, blocks[0].ID) + count, err = store.GetSubscribersCountForBlock(blocks[0].ID) require.NoError(t, err, "get subscribers for block should not error") assert.Zero(t, count) }) t.Run("get subscribers for invalid block", func(t *testing.T) { - subs, err := store.GetSubscribersForBlock(container, "bogus") + subs, err := store.GetSubscribersForBlock("bogus") require.NoError(t, err, "get subscribers for block should not error") assert.Empty(t, subs) }) diff --git a/server/services/store/storetests/system.go b/server/services/store/storetests/system.go index 8606cd21b..d6fa83a99 100644 --- a/server/services/store/storetests/system.go +++ b/server/services/store/storetests/system.go @@ -10,7 +10,8 @@ import ( // these system settings are created when running the data migrations, // so they will be present after the tests setup. var dataMigrationSystemSettings = map[string]string{ - "UniqueIDsMigrationComplete": "true", + "UniqueIDsMigrationComplete": "true", + "CategoryUuidIdMigrationComplete": "true", } func addBaseSettings(m map[string]string) map[string]string { @@ -25,18 +26,14 @@ func addBaseSettings(m map[string]string) map[string]string { } func StoreTestSystemStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { - container := store.Container{ - WorkspaceID: "0", - } - t.Run("SetGetSystemSettings", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testSetGetSystemSettings(t, store, container) + testSetGetSystemSettings(t, store) }) } -func testSetGetSystemSettings(t *testing.T, store store.Store, _ /*container*/ store.Container) { +func testSetGetSystemSettings(t *testing.T, store store.Store) { t.Run("Get empty settings", func(t *testing.T) { settings, err := store.GetSystemSettings() require.NoError(t, err) diff --git a/server/services/store/storetests/teams.go b/server/services/store/storetests/teams.go new file mode 100644 index 000000000..d94f44624 --- /dev/null +++ b/server/services/store/storetests/teams.go @@ -0,0 +1,121 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package storetests + +import ( + "fmt" + + "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/utils" + + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/focalboard/server/services/store" +) + +func StoreTestTeamStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { + t.Run("UpsertTeamSignupToken", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testUpsertTeamSignupToken(t, store) + }) + + t.Run("UpsertTeamSettings", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testUpsertTeamSettings(t, store) + }) + + t.Run("GetAllTeams", func(t *testing.T) { + store, tearDown := setup(t) + defer tearDown() + testGetAllTeams(t, store) + }) +} + +func testUpsertTeamSignupToken(t *testing.T, store store.Store) { + t.Run("Insert and update team with signup token", func(t *testing.T) { + teamID := "0" + team := &model.Team{ + ID: teamID, + SignupToken: utils.NewID(utils.IDTypeToken), + } + + // insert + err := store.UpsertTeamSignupToken(*team) + require.NoError(t, err) + + got, err := store.GetTeam(teamID) + require.NoError(t, err) + require.Equal(t, team.ID, got.ID) + require.Equal(t, team.SignupToken, got.SignupToken) + + // update signup token + team.SignupToken = utils.NewID(utils.IDTypeToken) + err = store.UpsertTeamSignupToken(*team) + require.NoError(t, err) + + got, err = store.GetTeam(teamID) + require.NoError(t, err) + require.Equal(t, team.ID, got.ID) + require.Equal(t, team.SignupToken, got.SignupToken) + }) +} + +func testUpsertTeamSettings(t *testing.T, store store.Store) { + t.Run("Insert and update team with settings", func(t *testing.T) { + teamID := "0" + team := &model.Team{ + ID: teamID, + Settings: map[string]interface{}{ + "field1": "A", + }, + } + + // insert + err := store.UpsertTeamSettings(*team) + require.NoError(t, err) + + got, err := store.GetTeam(teamID) + require.NoError(t, err) + require.Equal(t, team.ID, got.ID) + require.Equal(t, team.Settings, got.Settings) + + // update settings + team.Settings = map[string]interface{}{ + "field1": "B", + } + err = store.UpsertTeamSettings(*team) + require.NoError(t, err) + + got2, err := store.GetTeam(teamID) + require.NoError(t, err) + require.Equal(t, team.ID, got2.ID) + require.Equal(t, team.Settings, got2.Settings) + require.Equal(t, got.SignupToken, got2.SignupToken) + }) +} + +func testGetAllTeams(t *testing.T, store store.Store) { + t.Run("Insert multiple team and get all teams", func(t *testing.T) { + // insert + teamCount := 10 + for i := 0; i < teamCount; i++ { + teamID := fmt.Sprintf("%d", i) + team := &model.Team{ + ID: teamID, + SignupToken: utils.NewID(utils.IDTypeToken), + } + + err := store.UpsertTeamSignupToken(*team) + require.NoError(t, err) + } + + got, err := store.GetAllTeams() + require.NoError(t, err) + require.Len(t, got, teamCount) + }) +} diff --git a/server/services/store/storetests/users.go b/server/services/store/storetests/users.go index da329d31c..d236a4d68 100644 --- a/server/services/store/storetests/users.go +++ b/server/services/store/storetests/users.go @@ -19,7 +19,7 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun t.Run("SetGetSystemSettings", func(t *testing.T) { store, tearDown := setup(t) defer tearDown() - testGetWorkspaceUsers(t, store) + testGetTeamUsers(t, store) }) t.Run("CreateAndGetUser", func(t *testing.T) { @@ -46,9 +46,9 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun }) } -func testGetWorkspaceUsers(t *testing.T, store store.Store) { - t.Run("GetWorkspaceUSers", func(t *testing.T) { - users, err := store.GetUsersByWorkspace("workspace_1") +func testGetTeamUsers(t *testing.T, store store.Store) { + t.Run("GetTeamUSers", func(t *testing.T) { + users, err := store.GetUsersByTeam("team_1") require.Equal(t, 0, len(users)) require.Equal(t, sql.ErrNoRows, err) @@ -67,7 +67,7 @@ func testGetWorkspaceUsers(t *testing.T, store store.Store) { }) }() - users, err = store.GetUsersByWorkspace("workspace_1") + users, err = store.GetUsersByTeam("team_1") require.Equal(t, 1, len(users)) require.Equal(t, "darth.vader", users[0].Username) require.NoError(t, err) diff --git a/server/services/store/storetests/util.go b/server/services/store/storetests/util.go index d1407845d..b7a01df60 100644 --- a/server/services/store/storetests/util.go +++ b/server/services/store/storetests/util.go @@ -29,26 +29,19 @@ func createTestUsers(t *testing.T, store store.Store, num int) []*model.User { return users } -func createTestBlocks(t *testing.T, store store.Store, container store.Container, userID string, num int) []*model.Block { +func createTestBlocks(t *testing.T, store store.Store, userID string, num int) []*model.Block { var blocks []*model.Block for i := 0; i < num; i++ { block := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - RootID: utils.NewID(utils.IDTypeBlock), - Type: "card", - CreatedBy: userID, - WorkspaceID: container.WorkspaceID, + ID: utils.NewID(utils.IDTypeBlock), + BoardID: utils.NewID(utils.IDTypeBoard), + Type: "card", + CreatedBy: userID, } - err := store.InsertBlock(container, block, userID) + err := store.InsertBlock(block, userID) require.NoError(t, err) blocks = append(blocks, block) } return blocks } - -func containerForWorkspace(workspaceID string) store.Container { - return store.Container{ - WorkspaceID: workspaceID, - } -} diff --git a/server/services/store/storetests/workspaces.go b/server/services/store/storetests/workspaces.go deleted file mode 100644 index 299328d74..000000000 --- a/server/services/store/storetests/workspaces.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "fmt" - "time" - - "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/utils" - - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/focalboard/server/services/store" -) - -func StoreTestWorkspaceStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { - t.Run("UpsertWorkspaceSignupToken", func(t *testing.T) { - store, tearDown := setup(t) - defer tearDown() - testUpsertWorkspaceSignupToken(t, store) - }) - - t.Run("UpsertWorkspaceSettings", func(t *testing.T) { - store, tearDown := setup(t) - defer tearDown() - testUpsertWorkspaceSettings(t, store) - }) - - t.Run("GetWorkspaceCount", func(t *testing.T) { - store, tearDown := setup(t) - defer tearDown() - testGetWorkspaceCount(t, store) - }) -} - -func testUpsertWorkspaceSignupToken(t *testing.T, store store.Store) { - t.Run("Insert and update workspace with signup token", func(t *testing.T) { - workspaceID := "0" - workspace := &model.Workspace{ - ID: workspaceID, - SignupToken: utils.NewID(utils.IDTypeToken), - } - - // insert - err := store.UpsertWorkspaceSignupToken(*workspace) - require.NoError(t, err) - - got, err := store.GetWorkspace(workspaceID) - require.NoError(t, err) - require.Equal(t, workspace.ID, got.ID) - require.Equal(t, workspace.SignupToken, got.SignupToken) - - // update signup token - workspace.SignupToken = utils.NewID(utils.IDTypeToken) - err = store.UpsertWorkspaceSignupToken(*workspace) - require.NoError(t, err) - - got, err = store.GetWorkspace(workspaceID) - require.NoError(t, err) - require.Equal(t, workspace.ID, got.ID) - require.Equal(t, workspace.SignupToken, got.SignupToken) - }) -} - -func testUpsertWorkspaceSettings(t *testing.T, store store.Store) { - t.Run("Insert and update workspace with settings", func(t *testing.T) { - workspaceID := "0" - workspace := &model.Workspace{ - ID: workspaceID, - Settings: map[string]interface{}{ - "field1": "A", - }, - } - - // insert - err := store.UpsertWorkspaceSettings(*workspace) - require.NoError(t, err) - - got, err := store.GetWorkspace(workspaceID) - require.NoError(t, err) - require.Equal(t, workspace.ID, got.ID) - require.Equal(t, workspace.Settings, got.Settings) - - // update settings - workspace.Settings = map[string]interface{}{ - "field1": "B", - } - err = store.UpsertWorkspaceSettings(*workspace) - require.NoError(t, err) - - got2, err := store.GetWorkspace(workspaceID) - require.NoError(t, err) - require.Equal(t, workspace.ID, got2.ID) - require.Equal(t, workspace.Settings, got2.Settings) - require.Equal(t, got.SignupToken, got2.SignupToken) - }) -} - -func testGetWorkspaceCount(t *testing.T, store store.Store) { - t.Run("Insert multiple workspace and get workspace count", func(t *testing.T) { - // insert - n := time.Now().Unix() % 10 - for i := 0; i < int(n); i++ { - workspaceID := fmt.Sprintf("%d", i) - workspace := &model.Workspace{ - ID: workspaceID, - SignupToken: utils.NewID(utils.IDTypeToken), - } - - err := store.UpsertWorkspaceSignupToken(*workspace) - require.NoError(t, err) - } - - got, err := store.GetWorkspaceCount() - require.NoError(t, err) - require.Equal(t, n, got) - }) -} diff --git a/server/swagger/swagger.yml b/server/swagger/swagger.yml index 761d651f6..23645e637 100644 --- a/server/swagger/swagger.yml +++ b/server/swagger/swagger.yml @@ -5,6 +5,10 @@ definitions: Block: description: Block is the basic data unit properties: + boardId: + description: The board id that the block belongs to + type: string + x-go-name: BoardID createAt: description: The creation time format: int64 @@ -37,10 +41,6 @@ definitions: description: The id for this block's parent block. Empty for root blocks type: string x-go-name: ParentID - rootId: - description: The id for this block's root block - type: string - x-go-name: RootID schema: description: The schema version of this block format: int64 @@ -57,25 +57,24 @@ definitions: format: int64 type: integer x-go-name: UpdateAt - workspaceId: - description: The workspace id that the block belongs to - type: string - x-go-name: WorkspaceID required: - id - - rootId - createdBy - modifiedBy - schema - type - createAt - updateAt - - workspaceId + - boardId type: object x-go-package: github.com/mattermost/focalboard/server/model BlockPatch: description: BlockPatch is a patch for modify blocks properties: + boardId: + description: The board id that the block belongs to + type: string + x-go-name: BoardID deletedFields: description: The block removed fields items: @@ -86,10 +85,6 @@ definitions: description: The id for this block's parent block. Empty for root blocks type: string x-go-name: ParentID - rootId: - description: The id for this block's root block - type: string - x-go-name: RootID schema: description: The schema version of this block format: int64 @@ -109,23 +104,6 @@ definitions: x-go-name: UpdatedFields type: object x-go-package: github.com/mattermost/focalboard/server/model - UserPropPatch: - description: UserConfigPatch is a patch for user config - properties: - deletedFields: - description: The config fields removed - items: - type: string - type: array - x-go-name: DeletedFields - updatedFields: - additionalProperties: - type: object - description: The updated config - type: object - x-go-name: UpdatedFields - type: object - x-go-package: github.com/mattermost/focalboard/server/model BlockPatchBatch: description: BlockPatchBatch is a batch of IDs and patches for modify blocks properties: @@ -147,6 +125,224 @@ definitions: title: BlockType represents a block type. type: string x-go-package: github.com/mattermost/focalboard/server/model + Board: + description: Board groups a set of blocks and its layout + properties: + cardProperties: + description: The properties of the board cards + items: + additionalProperties: + type: object + type: object + type: array + x-go-name: CardProperties + channelId: + description: The ID of the channel that the board was created from + type: string + x-go-name: ChannelID + columnCalculations: + additionalProperties: + type: object + description: The calculations on the board's cards + type: object + x-go-name: ColumnCalculations + createAt: + description: The creation time + format: int64 + type: integer + x-go-name: CreateAt + createdBy: + description: The ID of the user that created the board + type: string + x-go-name: CreatedBy + deleteAt: + description: The deleted time. Set to indicate this block is deleted + format: int64 + type: integer + x-go-name: DeleteAt + description: + description: The description of the board + type: string + x-go-name: Description + icon: + description: The icon of the board + type: string + x-go-name: Icon + id: + description: The ID for the board + type: string + x-go-name: ID + isTemplate: + description: Marks the template boards + type: boolean + x-go-name: IsTemplate + modifiedBy: + description: The ID of the last user that updated the board + type: string + x-go-name: ModifiedBy + properties: + additionalProperties: + type: object + description: The properties of the board + type: object + x-go-name: Properties + showDescription: + description: Indicates if the board shows the description on the interface + type: boolean + x-go-name: ShowDescription + teamId: + description: The ID of the team that the board belongs to + type: string + x-go-name: TeamID + templateVersion: + description: Marks the template boards + format: int64 + type: integer + x-go-name: TemplateVersion + title: + description: The title of the board + type: string + x-go-name: Title + type: + $ref: '#/definitions/BoardType' + updateAt: + description: The last modified time + format: int64 + type: integer + x-go-name: UpdateAt + required: + - id + - teamId + - createdBy + - modifiedBy + - type + - createAt + - updateAt + type: object + x-go-package: github.com/mattermost/focalboard/server/model + BoardMember: + description: BoardMember stores the information of the membership of a user on a board + properties: + boardId: + description: The ID of the board + type: string + x-go-name: BoardID + roles: + description: The independent roles of the user on the board + type: string + x-go-name: Roles + schemeAdmin: + description: Marks the user as an admin of the board + type: boolean + x-go-name: SchemeAdmin + schemeCommenter: + description: Marks the user as an commenter of the board + type: boolean + x-go-name: SchemeCommenter + schemeEditor: + description: Marks the user as an editor of the board + type: boolean + x-go-name: SchemeEditor + schemeViewer: + description: Marks the user as an viewer of the board + type: boolean + x-go-name: SchemeViewer + userId: + description: The ID of the user + type: string + x-go-name: UserID + required: + - boardId + - userId + - schemeAdmin + - schemeEditor + - schemeCommenter + - schemeViewer + type: object + x-go-package: github.com/mattermost/focalboard/server/model + BoardPatch: + description: BoardPatch is a patch for modify boards + properties: + deletedCardProperties: + description: The board removed card properties + items: + type: string + type: array + x-go-name: DeletedCardProperties + deletedColumnCalculations: + description: The board deleted column calculations + items: + type: string + type: array + x-go-name: DeletedColumnCalculations + deletedProperties: + description: The board removed properties + items: + type: string + type: array + x-go-name: DeletedProperties + description: + description: The description of the board + type: string + x-go-name: Description + icon: + description: The icon of the board + type: string + x-go-name: Icon + showDescription: + description: Indicates if the board shows the description on the interface + type: boolean + x-go-name: ShowDescription + title: + description: The title of the board + type: string + x-go-name: Title + type: + $ref: '#/definitions/BoardType' + updatedCardProperties: + description: The board updated card properties + items: + additionalProperties: + type: object + type: object + type: array + x-go-name: UpdatedCardProperties + updatedColumnCalculations: + additionalProperties: + type: object + description: The board updated column calculations + type: object + x-go-name: UpdatedColumnCalculations + updatedProperties: + additionalProperties: + type: object + description: The board updated properties + type: object + x-go-name: UpdatedProperties + type: object + x-go-package: github.com/mattermost/focalboard/server/model + BoardType: + type: string + x-go-package: github.com/mattermost/focalboard/server/model + BoardsAndBlocks: + description: |- + BoardsAndBlocks is used to operate over boards and blocks at the + same time + properties: + blocks: + description: The blocks + items: + $ref: '#/definitions/Block' + type: array + x-go-name: Blocks + boards: + description: The boards + items: + $ref: '#/definitions/Board' + type: array + x-go-name: Boards + type: object + x-go-package: github.com/mattermost/focalboard/server/model ChangePasswordRequest: description: ChangePasswordRequest is a user password change request properties: @@ -163,6 +359,28 @@ definitions: - newPassword type: object x-go-package: github.com/mattermost/focalboard/server/api + DeleteBoardsAndBlocks: + description: |- + DeleteBoardsAndBlocks is used to list the boards and blocks to + delete on a request + properties: + blocks: + description: The blocks + items: + type: string + type: array + x-go-name: Blocks + boards: + description: The boards + items: + type: string + type: array + x-go-name: Boards + required: + - boards + - blocks + type: object + x-go-package: github.com/mattermost/focalboard/server/model ErrorResponse: description: ErrorResponse is an error response properties: @@ -248,18 +466,49 @@ definitions: format: int64 type: integer x-go-name: NotifyAt - workspace_id: - description: WorkspaceID is id of workspace the block belongs to - type: string - x-go-name: WorkspaceID required: - block_type - block_id - - workspace_id - create_at - notify_at type: object x-go-package: github.com/mattermost/focalboard/server/model + PatchBoardsAndBlocks: + description: |- + PatchBoardsAndBlocks is used to patch multiple boards and blocks on + a single request + properties: + blockIDs: + description: The block IDs to patch + items: + type: string + type: array + x-go-name: BlockIDs + blockPatches: + description: The block patches + items: + $ref: '#/definitions/BlockPatch' + type: array + x-go-name: BlockPatches + boardIDs: + description: The board IDs to patch + items: + type: string + type: array + x-go-name: BoardIDs + boardPatches: + description: The board patches + items: + $ref: '#/definitions/BoardPatch' + type: array + x-go-name: BoardPatches + required: + - boardIDs + - boardPatches + - blockIDs + - blockPatches + type: object + x-go-package: github.com/mattermost/focalboard/server/model RegisterRequest: description: RegisterRequest is a user registration request properties: @@ -319,8 +568,7 @@ definitions: type: object x-go-package: github.com/mattermost/focalboard/server/model Subscriber: - description: Subscriber is an entity (e.g. user, channel) that can subscribe to - events from boards, cards, etc + description: Subscriber is an entity (e.g. user, channel) that can subscribe to events from boards, cards, etc properties: notified_at: description: NotifiedAt is the timestamp this subscriber was last notified @@ -355,14 +603,12 @@ definitions: type: integer x-go-name: CreateAt deleteAt: - description: DeleteAt is the timestamp this subscription was deleted, or zero - if not deleted + description: DeleteAt is the timestamp this subscription was deleted, or zero if not deleted format: int64 type: integer x-go-name: DeleteAt notifiedAt: - description: NotifiedAt is the timestamp of the last notification sent for - this subscription + description: NotifiedAt is the timestamp of the last notification sent for this subscription format: int64 type: integer x-go-name: NotifiedAt @@ -372,14 +618,9 @@ definitions: x-go-name: SubscriberID subscriberType: $ref: '#/definitions/SubscriberType' - workspaceId: - description: WorkspaceID is id of the workspace the block belongs to - type: string - x-go-name: WorkspaceID required: - blockType - blockId - - workspaceId - subscriberType - subscriberId - notifiedAt @@ -388,6 +629,43 @@ definitions: title: Subscription is a subscription to a board, card, etc, for a user or channel. type: object x-go-package: github.com/mattermost/focalboard/server/model + Team: + description: Team is information global to a team + properties: + id: + description: ID of the team + type: string + x-go-name: ID + modifiedBy: + description: ID of user who last modified this + type: string + x-go-name: ModifiedBy + settings: + additionalProperties: + type: object + description: Team settings + type: object + x-go-name: Settings + signupToken: + description: Token required to register new users + type: string + x-go-name: SignupToken + title: + description: Title of the team + type: string + x-go-name: Title + updateAt: + description: Updated time + format: int64 + type: integer + x-go-name: UpdateAt + required: + - id + - signupToken + - modifiedBy + - updateAt + type: object + x-go-package: github.com/mattermost/focalboard/server/model User: description: User is a user properties: @@ -434,74 +712,23 @@ definitions: - is_bot type: object x-go-package: github.com/mattermost/focalboard/server/model - UserWorkspace: - description: |- - UserWorkspace is a summary of a single association between - a user and a workspace + UserPropPatch: + description: UserPropPatch is a user property patch properties: - boardCount: - description: Number of boards in the workspace - format: int64 - type: integer - x-go-name: BoardCount - id: - description: ID of the workspace - type: string - x-go-name: ID - title: - description: Title of the workspace - type: string - x-go-name: Title - required: - - id - type: object - x-go-package: github.com/mattermost/focalboard/server/model - Workspace: - description: Workspace is information global to a workspace - properties: - id: - description: ID of the workspace - type: string - x-go-name: ID - modifiedBy: - description: ID of user who last modified this - type: string - x-go-name: ModifiedBy - settings: + deletedFields: + description: The user prop removed fields + items: + type: string + type: array + x-go-name: DeletedFields + updatedFields: additionalProperties: - type: object - description: Workspace settings + type: string + description: The user prop updated fields type: object - x-go-name: Settings - signupToken: - description: Token required to register new users - type: string - x-go-name: SignupToken - title: - description: Title of the workspace - type: string - x-go-name: Title - updateAt: - description: Updated time - format: int64 - type: integer - x-go-name: UpdateAt - required: - - id - - signupToken - - modifiedBy - - updateAt + x-go-name: UpdatedFields type: object x-go-package: github.com/mattermost/focalboard/server/model - OnboardingResponse: - description: OnboardResponse contains basic data required by the client to complete the onboarding - properties: - workspaceID: - description: the workspace to send to user to, to start the onboarding tour - type: string - boardID: - description: the board to send to user to, to start the onboarding tour - type: string host: localhost info: contact: @@ -515,6 +742,613 @@ info: title: Focalboard Server version: 1.0.0 paths: + /api/v1/boards: + post: + description: Creates a new board + operationId: createBoard + parameters: + - description: the board to create + in: body + name: Body + required: true + schema: + $ref: '#/definitions/Board' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/Board' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards-and-blocks: + delete: + description: Deletes boards and blocks + operationId: deleteBoardsAndBlocks + parameters: + - description: the boards and blocks to delete + in: body + name: Body + required: true + schema: + $ref: '#/definitions/DeleteBoardsAndBlocks' + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + patch: + description: Patches a set of related boards and blocks + operationId: patchBoardsAndBlocks + parameters: + - description: the patches for the boards and blocks + in: body + name: Body + required: true + schema: + $ref: '#/definitions/PatchBoardsAndBlocks' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/BoardsAndBlocks' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + post: + description: Creates new boards and blocks + operationId: insertBoardsAndBlocks + parameters: + - description: the boards and blocks to create + in: body + name: Body + required: true + schema: + $ref: '#/definitions/BoardsAndBlocks' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/BoardsAndBlocks' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}: + delete: + description: Removes a board + operationId: deleteBoard + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + get: + description: Returns a board + operationId: getBoard + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/Board' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + patch: + description: Partially updates a board + operationId: patchBoard + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: board patch to apply + in: body + name: Body + required: true + schema: + $ref: '#/definitions/BoardPatch' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/Board' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/archive/export: + get: + operationId: archiveExportBoard + parameters: + - description: Id of board to export + in: path + name: boardID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + summary: Exports an archive of all blocks for one boards. + /api/v1/boards/{boardID}/archive/import: + post: + consumes: + - multipart/form-data + operationId: archiveImport + parameters: + - description: Workspace ID + in: path + name: boardID + required: true + type: string + - description: archive file to import + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + summary: Import an archive of boards. + /api/v1/boards/{boardID}/blocks: + get: + description: Returns blocks + operationId: getBlocks + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: ID of parent block, omit to specify all blocks + in: query + name: parent_id + type: string + - description: Type of blocks to return, omit to specify all types + in: query + name: type + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Block' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + post: + description: |- + Insert blocks. The specified IDs will only be used to link + blocks with existing ones, the rest will be replaced by server + generated IDs + operationId: updateBlocks + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: array of blocks to insert or update + in: body + name: Body + required: true + schema: + items: + $ref: '#/definitions/Block' + type: array + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Block' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/blocks/: + patch: + description: Partially updates batch of blocks + operationId: patchBlocks + parameters: + - description: Workspace ID + in: path + name: boardID + required: true + type: string + - description: block Ids and block patches to apply + in: body + name: Body + required: true + schema: + $ref: '#/definitions/BlockPatchBatch' + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/blocks/{blockID}: + delete: + description: Deletes a block + operationId: deleteBlock + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: ID of block to delete + in: path + name: blockID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + patch: + description: Partially updates a block + operationId: patchBlock + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: ID of block to patch + in: path + name: blockID + required: true + type: string + - description: block patch to apply + in: body + name: Body + required: true + schema: + $ref: '#/definitions/BlockPatch' + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/blocks/{blockID}/duplicate: + post: + description: Returns the new created blocks + operationId: duplicateBlock + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: Block ID + in: path + name: blockID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Block' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/blocks/{blockID}/subtree: + get: + description: Returns the blocks of a subtree + operationId: getSubTree + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: The ID of the root block of the subtree + in: path + name: blockID + required: true + type: string + - description: The number of levels to return. 2 or 3. Defaults to 2. + in: query + maximum: 3 + minimum: 2 + name: l + type: integer + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Block' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/blocks/export: + get: + description: Returns all blocks of a board + operationId: exportBlocks + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Block' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/blocks/import: + post: + description: Import blocks on a given board + operationId: importBlocks + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: array of blocks to import + in: body + name: Body + required: true + schema: + items: + $ref: '#/definitions/Block' + type: array + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/duplicate: + post: + description: Returns the new created board and all the blocks + operationId: duplicateBoard + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/BoardsAndBlocks' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/members: + get: + description: Returns the members of the board + operationId: getMembersForBoard + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/BoardMember' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/members/{userID}: + delete: + description: Deletes a member from a board + operationId: deleteMember + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: User ID + in: path + name: userID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/boards/{boardID}/sharing: + get: + description: Returns sharing information for a board + operationId: getSharing + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/Sharing' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + post: + description: Sets sharing information for a board + operationId: postSharing + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: sharing information for a root block + in: body + name: Body + required: true + schema: + $ref: '#/definitions/Sharing' + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] /api/v1/login: post: description: Login user @@ -578,6 +1412,336 @@ paths: description: internal error schema: $ref: '#/definitions/ErrorResponse' + /api/v1/subscriptions: + post: + operationId: createSubscription + parameters: + - description: subscription definition + in: body + name: Body + required: true + schema: + $ref: '#/definitions/Subscription' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/User' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + summary: Creates a subscription to a block for a user. The user will receive change notifications for the block. + /api/v1/subscriptions/{blockID}/{subscriberID}: + delete: + operationId: deleteSubscription + parameters: + - description: Block ID + in: path + name: blockID + required: true + type: string + - description: Subscriber ID + in: path + name: subscriberID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + summary: Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block. + /api/v1/subscriptions/{subscriberID}: + get: + operationId: getSubscriptions + parameters: + - description: Subscriber ID + in: path + name: subscriberID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/User' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + summary: Gets subscriptions for a user. + /api/v1/team/{teamID}/onboard: + post: + operationId: onboard + parameters: + - description: Team ID + in: path + name: teamID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/OnboardingResponse' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + summary: Onboards a user on Boards. + /api/v1/teams: + get: + description: Returns information of all the teams + operationId: getTeams + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Team' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/teams/{teamID}: + get: + description: Returns information of the root team + operationId: getTeam + parameters: + - description: Team ID + in: path + name: teamID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/Team' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/teams/{teamID}/archive/export: + get: + operationId: archiveExportTeam + parameters: + - description: Id of team + in: path + name: teamID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + summary: Exports an archive of all blocks for all the boards in a team. + /api/v1/teams/{teamID}/boards: + get: + description: Returns team boards + operationId: getBoards + parameters: + - description: Team ID + in: path + name: teamID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Board' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/teams/{teamID}/boards/{boardID}/files: + post: + consumes: + - multipart/form-data + description: Upload a binary file, attached to a root block + operationId: uploadFile + parameters: + - description: ID of the team + in: path + name: teamID + required: true + type: string + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: The file to upload + in: formData + name: uploaded file + type: file + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/FileUploadResponse' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/teams/{teamID}/boards/search: + get: + description: Returns the boards that match with a search term + operationId: searchBoards + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: Team ID + in: path + name: teamID + required: true + type: string + - description: The search term. Must have at least one character + in: query + name: q + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Board' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/teams/{teamID}/regenerate_signup_token: + post: + description: Regenerates the signup token for the root team + operationId: regenerateSignupToken + parameters: + - description: Team ID + in: path + name: teamID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/teams/{teamID}/templates: + get: + description: Returns team templates + operationId: getTemplates + parameters: + - description: Team ID + in: path + name: teamID + required: true + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/Board' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /api/v1/teams/{teamID}/users: + get: + description: Returns team users + operationId: getTeamUsers + parameters: + - description: Team ID + in: path + name: teamID + required: true + type: string + - description: string to filter users list + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + items: + $ref: '#/definitions/User' + type: array + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] /api/v1/users/{userID}: get: description: Returns a user @@ -632,6 +1796,33 @@ paths: $ref: '#/definitions/ErrorResponse' security: - BearerAuth: [] + /api/v1/users/{userID}/config: + patch: + description: Updates user config + operationId: updateUserConfig + parameters: + - description: User ID + in: path + name: userID + required: true + type: string + - description: User config patch to apply + in: body + name: Body + required: true + schema: + $ref: '#/definitions/UserPropPatch' + produces: + - application/json + responses: + "200": + description: success + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] /api/v1/users/me: get: description: Returns the currently logged-in user @@ -649,169 +1840,17 @@ paths: $ref: '#/definitions/ErrorResponse' security: - BearerAuth: [] - /api/v1/workspaces/{workspaceID}: - get: - description: Returns information of the root workspace - operationId: getWorkspace - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Workspace' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/{rootID}/files: + /api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete: post: - consumes: - - multipart/form-data - description: Upload a binary file, attached to a root block - operationId: uploadFile + description: Undeletes a block + operationId: undeleteBlock parameters: - description: Workspace ID in: path name: workspaceID required: true type: string - - description: ID of the root block - in: path - name: rootID - required: true - type: string - - description: The file to upload - in: formData - name: uploaded file - type: file - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/FileUploadResponse' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/blocks: - get: - description: Returns blocks - operationId: getBlocks - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: ID of parent block, omit to specify all blocks - in: query - name: parent_id - type: string - - description: Type of blocks to return, omit to specify all types - in: query - name: type - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Block' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - post: - description: |- - Insert blocks. The specified IDs will only be used to link - blocks with existing ones, the rest will be replaced by server - generated IDs - operationId: updateBlocks - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: array of blocks to insert or update - in: body - name: Body - required: true - schema: - items: - $ref: '#/definitions/Block' - type: array - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Block' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/blocks/: - patch: - description: Partially updates batch of blocks - operationId: patchBlocks - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: block Ids and block patches to apply - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BlockPatchBatch' - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/blocks/{blockID}: - delete: - description: Deletes a block - operationId: deleteBlock - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: ID of block to delete + - description: ID of block to undelete in: path name: blockID required: true @@ -827,332 +1866,14 @@ paths: $ref: '#/definitions/ErrorResponse' security: - BearerAuth: [] - patch: - description: Partially updates a block - operationId: patchBlock - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: ID of block to patch - in: path - name: blockID - required: true - type: string - - description: block patch to apply - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BlockPatch' - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/blocks/{blockID}/subtree: - get: - description: Returns the blocks of a subtree - operationId: getSubTree - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: The ID of the root block of the subtree - in: path - name: blockID - required: true - type: string - - description: The number of levels to return. 2 or 3. Defaults to 2. - in: query - maximum: 3 - minimum: 2 - name: l - type: integer - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Block' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/blocks/export: - get: - description: Returns all blocks - operationId: exportBlocks - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Block' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/blocks/import: - post: - description: Import blocks - operationId: importBlocks - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: array of blocks to import - in: body - name: Body - required: true - schema: - items: - $ref: '#/definitions/Block' - type: array - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/regenerate_signup_token: - post: - description: Regenerates the signup token for the root workspace - operationId: regenerateSignupToken - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/sharing/{rootID}: - get: - description: Returns sharing information for a root block - operationId: getSharing - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: ID of the root block - in: path - name: rootID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Sharing' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - post: - description: Sets sharing information for a root block - operationId: postSharing - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: ID of the root block - in: path - name: rootID - required: true - type: string - - description: sharing information for a root block - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Sharing' - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /api/v1/workspaces/{workspaceID}/subscriptions: - post: - operationId: createSubscription - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: subscription definition - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Subscription' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/User' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Creates a subscription to a block for a user. The user will receive - change notifications for the block. - /api/v1/workspaces/{workspaceID}/subscriptions/{blockID}/{subscriberID}: - delete: - operationId: deleteSubscription - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: Block ID - in: path - name: blockID - required: true - type: string - - description: Subscriber ID - in: path - name: subscriberID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Deletes a subscription a user has for a a block. The user will no longer - receive change notifications for the block. - /api/v1/workspaces/{workspaceID}/subscriptions/{subscriberID}: - get: - operationId: getSubscriptions - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - - description: Subscriber ID - in: path - name: subscriberID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/User' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Gets subscriptions for a user. - /api/v1/workspaces/{workspaceID}/users: - get: - description: Returns workspace users - operationId: getWorkspaceUsers - parameters: - - description: Workspace ID - in: path - name: workspaceID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/User' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /workspaces/{workspaceID}/{rootID}/{fileID}: + /boards/{boardID}/{rootID}/{fileID}: get: description: Returns the contents of an uploaded file operationId: getFile parameters: - - description: Workspace ID + - description: Board ID in: path - name: workspaceID + name: boardID required: true type: string - description: ID of the root block @@ -1169,6 +1890,7 @@ paths: - application/json - image/jpg - image/png + - image/gif responses: "200": description: success @@ -1178,6 +1900,69 @@ paths: $ref: '#/definitions/ErrorResponse' security: - BearerAuth: [] + /boards/{boardID}/members: + post: + description: Adds a new member to a board + operationId: addMember + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: membership to replace the current one with + in: body + name: Body + required: true + schema: + $ref: '#/definitions/BoardMember' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/BoardMember' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] + /boards/{boardID}/members/{userID}: + put: + description: Updates a board member + operationId: updateMember + parameters: + - description: Board ID + in: path + name: boardID + required: true + type: string + - description: User ID + in: path + name: userID + required: true + type: string + - description: membership to replace the current one with + in: body + name: Body + required: true + schema: + $ref: '#/definitions/BoardMember' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/BoardMember' + default: + description: internal error + schema: + $ref: '#/definitions/ErrorResponse' + security: + - BearerAuth: [] produces: - application/json schemes: @@ -1185,8 +1970,7 @@ schemes: - https securityDefinitions: BearerAuth: - description: 'Pass session token using Bearer authentication, e.g. set header - "Authorization: Bearer "' + description: 'Pass session token using Bearer authentication, e.g. set header "Authorization: Bearer "' in: header name: Authorization type: apiKey diff --git a/server/utils/callbackqueue.go b/server/utils/callbackqueue.go new file mode 100644 index 000000000..1fb4e970a --- /dev/null +++ b/server/utils/callbackqueue.go @@ -0,0 +1,129 @@ +package utils + +import ( + "context" + "sync/atomic" + "time" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +// CallbackFunc is a func that can enqueued in the callback queue and will be +// called when dequeued. +type CallbackFunc func() error + +// CallbackQueue provides a simple thread pool for processing callbacks. Callbacks will +// be executed in the order in which they are enqueued, but no guarantees are provided +// regarding the order in which they finish (unless poolSize == 1). +type CallbackQueue struct { + name string + poolSize int + + queue chan CallbackFunc + done chan struct{} + alive chan int + + idone uint32 + + logger *mlog.Logger +} + +// NewCallbackQueue creates a new CallbackQueue and starts a thread pool to service it. +func NewCallbackQueue(name string, queueSize int, poolSize int, logger *mlog.Logger) *CallbackQueue { + cn := &CallbackQueue{ + name: name, + poolSize: poolSize, + queue: make(chan CallbackFunc, queueSize), + done: make(chan struct{}), + alive: make(chan int, poolSize), + logger: logger, + } + + for i := 0; i < poolSize; i++ { + go cn.loop(i) + } + + return cn +} + +// Shutdown stops accepting enqueues and exits all pool threads. This method waits +// as long as the context allows for the threads to exit. +// Returns true if the pool exited, false on timeout. +func (cn *CallbackQueue) Shutdown(context context.Context) bool { + if !atomic.CompareAndSwapUint32(&cn.idone, 0, 1) { + // already shutdown + return true + } + + // signal threads to exit + close(cn.done) + + // wait for the threads to exit or timeout + count := 0 + for count < cn.poolSize { + select { + case <-cn.alive: + count++ + case <-context.Done(): + return false + } + } + + // try to drain any remaining callbacks + for { + select { + case f := <-cn.queue: + cn.exec(f) + case <-context.Done(): + return false + default: + return true + } + } +} + +// Enqueue adds a callback to the queue. +func (cn *CallbackQueue) Enqueue(f CallbackFunc) { + if atomic.LoadUint32(&cn.idone) != 0 { + cn.logger.Debug("CallbackQueue skipping enqueue, notifier is shutdown", mlog.String("name", cn.name)) + return + } + + select { + case cn.queue <- f: + default: + start := time.Now() + cn.queue <- f + dur := time.Since(start) + cn.logger.Warn("CallbackQueue queue backlog", mlog.String("name", cn.name), mlog.Duration("wait_time", dur)) + } +} + +func (cn *CallbackQueue) loop(id int) { + defer func() { + cn.logger.Trace("CallbackQueue thread exited", mlog.String("name", cn.name), mlog.Int("id", id)) + cn.alive <- id + }() + + for { + select { + case f := <-cn.queue: + cn.exec(f) + case <-cn.done: + return + } + } +} + +func (cn *CallbackQueue) exec(f CallbackFunc) { + // don't let a panic in the callback exit the thread. + defer func() { + if r := recover(); r != nil { + cn.logger.Error("CallbackQueue callback panic", mlog.String("name", cn.name), mlog.Any("panic", r)) + } + }() + + if err := f(); err != nil { + cn.logger.Error("CallbackQueue callback error", mlog.String("name", cn.name), mlog.Err(err)) + } +} diff --git a/server/utils/callbackqueue_test.go b/server/utils/callbackqueue_test.go new file mode 100644 index 000000000..25e5e02af --- /dev/null +++ b/server/utils/callbackqueue_test.go @@ -0,0 +1,64 @@ +package utils + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +func Test_newChangeNotifier(t *testing.T) { + logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug) + + t.Run("startup, shutdown", func(t *testing.T) { + cn := NewCallbackQueue("test1", 100, 5, logger) + + var callbackCount int32 + callback := func() error { + atomic.AddInt32(&callbackCount, 1) + return nil + } + + const loops = 500 + for i := 0; i < loops; i++ { + cn.Enqueue(callback) + // don't peg the cpu + if i%20 == 0 { + time.Sleep(time.Millisecond * 1) + } + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + ok := cn.Shutdown(ctx) + assert.True(t, ok, "shutdown should return true (no timeout)") + + assert.Equal(t, int32(loops), atomic.LoadInt32(&callbackCount)) + }) + + t.Run("handle panic", func(t *testing.T) { + cn := NewCallbackQueue("test2", 100, 5, logger) + + var callbackCount int32 + callback := func() error { + atomic.AddInt32(&callbackCount, 1) + panic("oh no!") + } + + const loops = 5 + for i := 0; i < loops; i++ { + cn.Enqueue(callback) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + ok := cn.Shutdown(ctx) + assert.True(t, ok, "shutdown should return true (no timeout)") + + assert.Equal(t, int32(loops), atomic.LoadInt32(&callbackCount)) + }) +} diff --git a/server/utils/links.go b/server/utils/links.go index 4b8e70a1b..f4adb0103 100644 --- a/server/utils/links.go +++ b/server/utils/links.go @@ -6,6 +6,6 @@ package utils import "fmt" // MakeCardLink creates fully qualified card links based on card id and parents. -func MakeCardLink(serverRoot string, workspace string, board string, card string) string { - return fmt.Sprintf("%s/workspace/%s/%s/0/%s/", serverRoot, workspace, board, card) +func MakeCardLink(serverRoot string, teamID string, boardID string, cardID string) string { + return fmt.Sprintf("%s/team/%s/%s/0/%s", serverRoot, teamID, boardID, cardID) } diff --git a/server/utils/utils.go b/server/utils/utils.go index 9c2db2d04..e0fe3d0af 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -10,15 +10,15 @@ import ( type IDType byte const ( - IDTypeNone IDType = '7' - IDTypeWorkspace IDType = 'w' - IDTypeBoard IDType = 'b' - IDTypeCard IDType = 'c' - IDTypeView IDType = 'v' - IDTypeSession IDType = 's' - IDTypeUser IDType = 'u' - IDTypeToken IDType = 'k' - IDTypeBlock IDType = 'a' + IDTypeNone IDType = '7' + IDTypeTeam IDType = 't' + IDTypeBoard IDType = 'b' + IDTypeCard IDType = 'c' + IDTypeView IDType = 'v' + IDTypeSession IDType = 's' + IDTypeUser IDType = 'u' + IDTypeToken IDType = 'k' + IDTypeBlock IDType = 'a' ) // NewId is a globally unique identifier. It is a [A-Z0-9] string 27 diff --git a/server/ws/adapter.go b/server/ws/adapter.go index 3a5774c14..707f96e30 100644 --- a/server/ws/adapter.go +++ b/server/ws/adapter.go @@ -1,3 +1,4 @@ +//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store package ws import ( @@ -5,19 +6,35 @@ import ( ) const ( - websocketActionAuth = "AUTH" - websocketActionSubscribeWorkspace = "SUBSCRIBE_WORKSPACE" - websocketActionUnsubscribeWorkspace = "UNSUBSCRIBE_WORKSPACE" - websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS" - websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS" - websocketActionUpdateBlock = "UPDATE_BLOCK" - websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG" - websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION" + websocketActionAuth = "AUTH" + websocketActionSubscribeTeam = "SUBSCRIBE_TEAM" + websocketActionUnsubscribeTeam = "UNSUBSCRIBE_TEAM" + websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS" + websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS" + websocketActionUpdateBoard = "UPDATE_BOARD" + websocketActionUpdateMember = "UPDATE_MEMBER" + websocketActionDeleteMember = "DELETE_MEMBER" + websocketActionUpdateBlock = "UPDATE_BLOCK" + websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG" + websocketActionUpdateCategory = "UPDATE_CATEGORY" + websocketActionUpdateCategoryBlock = "UPDATE_BLOCK_CATEGORY" + websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION" ) -type Adapter interface { - BroadcastBlockChange(workspaceID string, block model.Block) - BroadcastBlockDelete(workspaceID, blockID, parentID string) - BroadcastConfigChange(clientConfig model.ClientConfig) - BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) +type Store interface { + GetBlock(blockID string) (*model.Block, error) + GetMembersForBoard(boardID string) ([]*model.BoardMember, error) +} + +type Adapter interface { + BroadcastBlockChange(teamID string, block model.Block) + BroadcastBlockDelete(teamID, blockID, boardID string) + BroadcastBoardChange(teamID string, board *model.Board) + BroadcastBoardDelete(teamID, boardID string) + BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) + BroadcastMemberDelete(teamID, boardID, userID string) + BroadcastConfigChange(clientConfig model.ClientConfig) + BroadcastCategoryChange(category model.Category) + BroadcastCategoryBlockChange(teamID, userID string, blockCategory model.BlockCategoryWebsocketData) + BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) } diff --git a/server/ws/common.go b/server/ws/common.go index e130ed0a3..3c7f6e7de 100644 --- a/server/ws/common.go +++ b/server/ws/common.go @@ -4,12 +4,35 @@ import ( "github.com/mattermost/focalboard/server/model" ) -// UpdateMsg is sent on block updates. -type UpdateMsg struct { +// UpdateCategoryMessage is sent on block updates. +type UpdateCategoryMessage struct { + Action string `json:"action"` + TeamID string `json:"teamId"` + Category *model.Category `json:"category,omitempty"` + BlockCategories *model.BlockCategoryWebsocketData `json:"blockCategories,omitempty"` +} + +// UpdateBlockMsg is sent on block updates. +type UpdateBlockMsg struct { Action string `json:"action"` + TeamID string `json:"teamId"` Block model.Block `json:"block"` } +// UpdateBoardMsg is sent on block updates. +type UpdateBoardMsg struct { + Action string `json:"action"` + TeamID string `json:"teamId"` + Board *model.Board `json:"board"` +} + +// UpdateMemberMsg is sent on membership updates. +type UpdateMemberMsg struct { + Action string `json:"action"` + TeamID string `json:"teamId"` + Member *model.BoardMember `json:"member"` +} + // UpdateSubscription is sent on subscription updates. type UpdateSubscription struct { Action string `json:"action"` @@ -18,9 +41,9 @@ type UpdateSubscription struct { // WebsocketCommand is an incoming command from the client. type WebsocketCommand struct { - Action string `json:"action"` - WorkspaceID string `json:"workspaceId"` - Token string `json:"token"` - ReadToken string `json:"readToken"` - BlockIDs []string `json:"blockIds"` + Action string `json:"action"` + TeamID string `json:"teamId"` + Token string `json:"token"` + ReadToken string `json:"readToken"` + BlockIDs []string `json:"blockIds"` } diff --git a/server/ws/helpers_test.go b/server/ws/helpers_test.go index 5a360d35b..fc529c7ec 100644 --- a/server/ws/helpers_test.go +++ b/server/ws/helpers_test.go @@ -13,16 +13,18 @@ import ( ) type TestHelper struct { - api *wsMocks.MockAPI - auth *authMocks.MockAuthInterface - ctrl *gomock.Controller - pa *PluginAdapter + api *wsMocks.MockAPI + auth *authMocks.MockAuthInterface + store *wsMocks.MockStore + ctrl *gomock.Controller + pa *PluginAdapter } func SetupTestHelper(t *testing.T) *TestHelper { ctrl := gomock.NewController(t) mockAPI := wsMocks.NewMockAPI(ctrl) mockAuth := authMocks.NewMockAuthInterface(ctrl) + mockStore := wsMocks.NewMockStore(ctrl) mockAPI.EXPECT().LogDebug(gomock.Any(), gomock.Any()).AnyTimes() mockAPI.EXPECT().LogInfo(gomock.Any(), gomock.Any()).AnyTimes() @@ -30,10 +32,11 @@ func SetupTestHelper(t *testing.T) *TestHelper { mockAPI.EXPECT().LogWarn(gomock.Any(), gomock.Any()).AnyTimes() return &TestHelper{ - api: mockAPI, - auth: mockAuth, - ctrl: ctrl, - pa: NewPluginAdapter(mockAPI, mockAuth, mlog.CreateConsoleTestLogger(true, mlog.LvlDebug)), + api: mockAPI, + auth: mockAuth, + store: mockStore, + ctrl: ctrl, + pa: NewPluginAdapter(mockAPI, mockAuth, mockStore, mlog.CreateConsoleTestLogger(true, mlog.LvlDebug)), } } @@ -43,16 +46,16 @@ func (th *TestHelper) ReceiveWebSocketMessage(webConnID, userID, action string, th.pa.WebSocketMessageHasBeenPosted(webConnID, userID, req) } -func (th *TestHelper) SubscribeWebConnToWorkspace(webConnID, userID, workspaceID string) { +func (th *TestHelper) SubscribeWebConnToTeam(webConnID, userID, teamID string) { th.auth.EXPECT(). - DoesUserHaveWorkspaceAccess(userID, workspaceID). + DoesUserHaveTeamAccess(userID, teamID). Return(true) - msgData := map[string]interface{}{"workspaceId": workspaceID} - th.ReceiveWebSocketMessage(webConnID, userID, websocketActionSubscribeWorkspace, msgData) + msgData := map[string]interface{}{"teamId": teamID} + th.ReceiveWebSocketMessage(webConnID, userID, websocketActionSubscribeTeam, msgData) } -func (th *TestHelper) UnsubscribeWebConnFromWorkspace(webConnID, userID, workspaceID string) { - msgData := map[string]interface{}{"workspaceId": workspaceID} - th.ReceiveWebSocketMessage(webConnID, userID, websocketActionUnsubscribeWorkspace, msgData) +func (th *TestHelper) UnsubscribeWebConnFromTeam(webConnID, userID, teamID string) { + msgData := map[string]interface{}{"teamId": teamID} + th.ReceiveWebSocketMessage(webConnID, userID, websocketActionUnsubscribeTeam, msgData) } diff --git a/server/ws/mocks/mockstore.go b/server/ws/mocks/mockstore.go new file mode 100644 index 000000000..c9034e181 --- /dev/null +++ b/server/ws/mocks/mockstore.go @@ -0,0 +1,65 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/focalboard/server/ws (interfaces: Store) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/focalboard/server/model" +) + +// MockStore is a mock of Store interface. +type MockStore struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore +} + +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore) EXPECT() *MockStoreMockRecorder { + return m.recorder +} + +// GetBlock mocks base method. +func (m *MockStore) GetBlock(arg0 string) (*model.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlock", arg0) + ret0, _ := ret[0].(*model.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlock indicates an expected call of GetBlock. +func (mr *MockStoreMockRecorder) GetBlock(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockStore)(nil).GetBlock), arg0) +} + +// GetMembersForBoard mocks base method. +func (m *MockStore) GetMembersForBoard(arg0 string) ([]*model.BoardMember, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMembersForBoard", arg0) + ret0, _ := ret[0].([]*model.BoardMember) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMembersForBoard indicates an expected call of GetMembersForBoard. +func (mr *MockStoreMockRecorder) GetMembersForBoard(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembersForBoard", reflect.TypeOf((*MockStore)(nil).GetMembersForBoard), arg0) +} diff --git a/server/ws/plugin_adapter.go b/server/ws/plugin_adapter.go index 6c7866b27..541785f52 100644 --- a/server/ws/plugin_adapter.go +++ b/server/ws/plugin_adapter.go @@ -19,16 +19,17 @@ import ( const websocketMessagePrefix = "custom_focalboard_" -var errMissingWorkspaceInCommand = fmt.Errorf("command doesn't contain workspaceId") +var errMissingTeamInCommand = fmt.Errorf("command doesn't contain teamId") type PluginAdapterInterface interface { + Adapter OnWebSocketConnect(webConnID, userID string) OnWebSocketDisconnect(webConnID, userID string) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mmModel.WebSocketRequest) BroadcastConfigChange(clientConfig model.ClientConfig) - BroadcastBlockChange(workspaceID string, block model.Block) - BroadcastBlockDelete(workspaceID, blockID, parentID string) - BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) + BroadcastBlockChange(teamID string, block model.Block) + BroadcastBlockDelete(teamID, blockID, parentID string) + BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) HandleClusterEvent(ev mmModel.PluginClusterEvent) } @@ -36,29 +37,31 @@ type PluginAdapter struct { api plugin.API auth auth.AuthInterface staleThreshold time.Duration + store Store logger *mlog.Logger listenersMU sync.RWMutex listeners map[string]*PluginAdapterClient listenersByUserID map[string][]*PluginAdapterClient - subscriptionsMU sync.RWMutex - listenersByWorkspace map[string][]*PluginAdapterClient - listenersByBlock map[string][]*PluginAdapterClient + subscriptionsMU sync.RWMutex + listenersByTeam map[string][]*PluginAdapterClient + listenersByBlock map[string][]*PluginAdapterClient } -func NewPluginAdapter(api plugin.API, auth auth.AuthInterface, logger *mlog.Logger) *PluginAdapter { +func NewPluginAdapter(api plugin.API, auth auth.AuthInterface, store Store, logger *mlog.Logger) *PluginAdapter { return &PluginAdapter{ - api: api, - auth: auth, - staleThreshold: 5 * time.Minute, - logger: logger, - listeners: make(map[string]*PluginAdapterClient), - listenersByUserID: make(map[string][]*PluginAdapterClient), - listenersByWorkspace: make(map[string][]*PluginAdapterClient), - listenersByBlock: make(map[string][]*PluginAdapterClient), - listenersMU: sync.RWMutex{}, - subscriptionsMU: sync.RWMutex{}, + api: api, + auth: auth, + store: store, + staleThreshold: 5 * time.Minute, + logger: logger, + listeners: make(map[string]*PluginAdapterClient), + listenersByUserID: make(map[string][]*PluginAdapterClient), + listenersByTeam: make(map[string][]*PluginAdapterClient), + listenersByBlock: make(map[string][]*PluginAdapterClient), + listenersMU: sync.RWMutex{}, + subscriptionsMU: sync.RWMutex{}, } } @@ -77,11 +80,11 @@ func (pa *PluginAdapter) GetListenersByUserID(userID string) []*PluginAdapterCli return pa.listenersByUserID[userID] } -func (pa *PluginAdapter) GetListenersByWorkspace(workspaceID string) []*PluginAdapterClient { +func (pa *PluginAdapter) GetListenersByTeam(teamID string) []*PluginAdapterClient { pa.subscriptionsMU.RLock() defer pa.subscriptionsMU.RUnlock() - return pa.listenersByWorkspace[workspaceID] + return pa.listenersByTeam[teamID] } func (pa *PluginAdapter) GetListenersByBlock(blockID string) []*PluginAdapterClient { @@ -103,9 +106,9 @@ func (pa *PluginAdapter) removeListener(pac *PluginAdapterClient) { pa.listenersMU.Lock() defer pa.listenersMU.Unlock() - // workspace subscriptions - for _, workspace := range pac.workspaces { - pa.removeListenerFromWorkspace(pac, workspace) + // team subscriptions + for _, team := range pac.teams { + pa.removeListenerFromTeam(pac, team) } // block subscriptions @@ -133,18 +136,18 @@ func (pa *PluginAdapter) removeExpiredForUserID(userID string) { } } -func (pa *PluginAdapter) removeListenerFromWorkspace(pac *PluginAdapterClient, workspaceID string) { - newWorkspaceListeners := []*PluginAdapterClient{} - for _, listener := range pa.GetListenersByWorkspace(workspaceID) { +func (pa *PluginAdapter) removeListenerFromTeam(pac *PluginAdapterClient, teamID string) { + newTeamListeners := []*PluginAdapterClient{} + for _, listener := range pa.GetListenersByTeam(teamID) { if listener.webConnID != pac.webConnID { - newWorkspaceListeners = append(newWorkspaceListeners, listener) + newTeamListeners = append(newTeamListeners, listener) } } pa.subscriptionsMU.Lock() - pa.listenersByWorkspace[workspaceID] = newWorkspaceListeners + pa.listenersByTeam[teamID] = newTeamListeners pa.subscriptionsMU.Unlock() - pac.unsubscribeFromWorkspace(workspaceID) + pac.unsubscribeFromTeam(teamID) } func (pa *PluginAdapter) removeListenerFromBlock(pac *PluginAdapterClient, blockID string) { @@ -161,29 +164,29 @@ func (pa *PluginAdapter) removeListenerFromBlock(pac *PluginAdapterClient, block pac.unsubscribeFromBlock(blockID) } -func (pa *PluginAdapter) subscribeListenerToWorkspace(pac *PluginAdapterClient, workspaceID string) { - if pac.isSubscribedToWorkspace(workspaceID) { +func (pa *PluginAdapter) subscribeListenerToTeam(pac *PluginAdapterClient, teamID string) { + if pac.isSubscribedToTeam(teamID) { return } pa.subscriptionsMU.Lock() - pa.listenersByWorkspace[workspaceID] = append(pa.listenersByWorkspace[workspaceID], pac) + pa.listenersByTeam[teamID] = append(pa.listenersByTeam[teamID], pac) pa.subscriptionsMU.Unlock() - pac.subscribeToWorkspace(workspaceID) + pac.subscribeToTeam(teamID) } -func (pa *PluginAdapter) unsubscribeListenerFromWorkspace(pac *PluginAdapterClient, workspaceID string) { - if !pac.isSubscribedToWorkspace(workspaceID) { +func (pa *PluginAdapter) unsubscribeListenerFromTeam(pac *PluginAdapterClient, teamID string) { + if !pac.isSubscribedToTeam(teamID) { return } - pa.removeListenerFromWorkspace(pac, workspaceID) + pa.removeListenerFromTeam(pac, teamID) } -func (pa *PluginAdapter) getUserIDsForWorkspace(workspaceID string) []string { +func (pa *PluginAdapter) getUserIDsForTeam(teamID string) []string { userMap := map[string]bool{} - for _, pac := range pa.GetListenersByWorkspace(workspaceID) { + for _, pac := range pa.GetListenersByTeam(teamID) { if pac.isActive() { userMap[pac.userID] = true } @@ -196,6 +199,58 @@ func (pa *PluginAdapter) getUserIDsForWorkspace(workspaceID string) []string { return userIDs } +func (pa *PluginAdapter) getUserIDsForTeamAndBoard(teamID, boardID string, ensureUserIDs ...string) []string { + userMap := map[string]bool{} + for _, pac := range pa.GetListenersByTeam(teamID) { + if pac.isActive() { + userMap[pac.userID] = true + } + } + + members, err := pa.store.GetMembersForBoard(boardID) + if err != nil { + pa.logger.Error("error getting members for board", + mlog.String("method", "getUserIDsForTeamAndBoard"), + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + ) + return nil + } + + // the list of users would be the intersection between the ones + // that are connected to the team and the board members that need + // to see the updates + userIDs := []string{} + for _, member := range members { + for userID := range userMap { + if userID == member.UserID { + userIDs = append(userIDs, userID) + } + } + } + + // if we don't have to make sure that some IDs are included, we + // can return at this point + if len(ensureUserIDs) == 0 { + return userIDs + } + + completeUserMap := map[string]bool{} + for _, id := range userIDs { + completeUserMap[id] = true + } + for _, id := range ensureUserIDs { + completeUserMap[id] = true + } + + completeUserIDs := []string{} + for id := range completeUserMap { + completeUserIDs = append(completeUserIDs, id) + } + + return completeUserIDs +} + //nolint:unused func (pa *PluginAdapter) unsubscribeListenerFromBlocks(pac *PluginAdapterClient, blockIDs []string) { for _, blockID := range blockIDs { @@ -219,7 +274,7 @@ func (pa *PluginAdapter) OnWebSocketConnect(webConnID, userID string) { inactiveAt: 0, webConnID: webConnID, userID: userID, - workspaces: []string{}, + teams: []string{}, blocks: []string{}, } @@ -243,10 +298,10 @@ func (pa *PluginAdapter) OnWebSocketDisconnect(webConnID, userID string) { func commandFromRequest(req *mmModel.WebSocketRequest) (*WebsocketCommand, error) { c := &WebsocketCommand{Action: strings.TrimPrefix(req.Action, websocketMessagePrefix)} - if workspaceID, ok := req.Data["workspaceId"]; ok { - c.WorkspaceID = workspaceID.(string) + if teamID, ok := req.Data["teamId"]; ok { + c.TeamID = teamID.(string) } else { - return nil, errMissingWorkspaceInCommand + return nil, errMissingTeamInCommand } if readToken, ok := req.Data["readToken"]; ok { @@ -296,29 +351,30 @@ func (pa *PluginAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, mlog.String("command", command.Action), mlog.String("webConnID", webConnID), mlog.String("userID", userID), - mlog.String("workspaceID", command.WorkspaceID), + mlog.String("teamID", command.TeamID), ) - case websocketActionSubscribeWorkspace: - pa.logger.Debug(`Command: SUBSCRIBE_WORKSPACE`, + case websocketActionSubscribeTeam: + pa.logger.Debug(`Command not implemented in plugin mode`, + mlog.String("command", command.Action), mlog.String("webConnID", webConnID), mlog.String("userID", userID), - mlog.String("workspaceID", command.WorkspaceID), + mlog.String("teamID", command.TeamID), ) - if !pa.auth.DoesUserHaveWorkspaceAccess(userID, command.WorkspaceID) { + if !pa.auth.DoesUserHaveTeamAccess(userID, command.TeamID) { return } - pa.subscribeListenerToWorkspace(pac, command.WorkspaceID) - case websocketActionUnsubscribeWorkspace: + pa.subscribeListenerToTeam(pac, command.TeamID) + case websocketActionUnsubscribeTeam: pa.logger.Debug(`Command: UNSUBSCRIBE_WORKSPACE`, mlog.String("webConnID", webConnID), mlog.String("userID", userID), - mlog.String("workspaceID", command.WorkspaceID), + mlog.String("teamID", command.TeamID), ) - pa.unsubscribeListenerFromWorkspace(pac, command.WorkspaceID) + pa.unsubscribeListenerFromTeam(pac, command.TeamID) } } @@ -340,59 +396,182 @@ func (pa *PluginAdapter) BroadcastConfigChange(pluginConfig model.ClientConfig) pa.sendMessageToAll(utils.StructToMap(pluginConfig)) } -// sendWorkspaceMessageSkipCluster sends a message to all the users -// with a websocket client connected to. -func (pa *PluginAdapter) sendWorkspaceMessageSkipCluster(event string, workspaceID string, payload map[string]interface{}) { - userIDs := pa.getUserIDsForWorkspace(workspaceID) +// sendTeamMessageSkipCluster sends a message to all the users +// with a websocket client subscribed to a given team. +func (pa *PluginAdapter) sendTeamMessageSkipCluster(event, teamID string, payload map[string]interface{}) { + userIDs := pa.getUserIDsForTeam(teamID) for _, userID := range userIDs { pa.api.PublishWebSocketEvent(event, payload, &mmModel.WebsocketBroadcast{UserId: userID}) } } -// sendWorkspaceMessage sends and propagates a message that is aimed -// for all the users that are subscribed to a given workspace. -func (pa *PluginAdapter) sendWorkspaceMessage(event string, workspaceID string, payload map[string]interface{}) { +// sendTeamMessage sends and propagates a message that is aimed +// for all the users that are subscribed to a given team. +func (pa *PluginAdapter) sendTeamMessage(event, teamID string, payload map[string]interface{}) { go func() { clusterMessage := &ClusterMessage{ - WorkspaceID: workspaceID, - Payload: payload, + TeamID: teamID, + Payload: payload, } pa.sendMessageToCluster("websocket_message", clusterMessage) }() - pa.sendWorkspaceMessageSkipCluster(event, workspaceID, payload) + pa.sendTeamMessageSkipCluster(event, teamID, payload) } -func (pa *PluginAdapter) BroadcastBlockChange(workspaceID string, block model.Block) { +// sendBoardMessageSkipCluster sends a message to all the users +// subscribed to a given team that belong to one of its boards. +func (pa *PluginAdapter) sendBoardMessageSkipCluster(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) { + userIDs := pa.getUserIDsForTeamAndBoard(teamID, boardID, ensureUserIDs...) + for _, userID := range userIDs { + pa.api.PublishWebSocketEvent(websocketActionUpdateBoard, payload, &mmModel.WebsocketBroadcast{UserId: userID}) + } +} + +// sendBoardMessage sends and propagates a message that is aimed for +// all the users that are subscribed to the board's team and are +// members of it too. +func (pa *PluginAdapter) sendBoardMessage(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) { + go func() { + clusterMessage := &ClusterMessage{ + TeamID: teamID, + BoardID: boardID, + Payload: payload, + EnsureUsers: ensureUserIDs, + } + + pa.sendMessageToCluster("websocket_message", clusterMessage) + }() + + pa.sendBoardMessageSkipCluster(teamID, boardID, payload, ensureUserIDs...) +} + +func (pa *PluginAdapter) BroadcastBlockChange(teamID string, block model.Block) { pa.logger.Debug("BroadcastingBlockChange", - mlog.String("workspaceID", workspaceID), + mlog.String("teamID", teamID), + mlog.String("boardID", block.BoardID), mlog.String("blockID", block.ID), ) - message := UpdateMsg{ + message := UpdateBlockMsg{ Action: websocketActionUpdateBlock, + TeamID: teamID, Block: block, } - pa.sendWorkspaceMessage(websocketActionUpdateBlock, workspaceID, utils.StructToMap(message)) + pa.sendBoardMessage(teamID, block.BoardID, utils.StructToMap(message)) } -func (pa *PluginAdapter) BroadcastBlockDelete(workspaceID, blockID, parentID string) { +func (pa *PluginAdapter) BroadcastCategoryChange(category model.Category) { + pa.logger.Debug("BroadcastCategoryChange", + mlog.String("userID", category.TeamID), + mlog.String("teamID", category.TeamID), + mlog.String("categoryID", category.ID), + ) + + message := UpdateCategoryMessage{ + Action: websocketActionUpdateCategory, + TeamID: category.TeamID, + Category: &category, + } + + pa.sendTeamMessage(websocketActionUpdateCategory, category.TeamID, utils.StructToMap(message)) +} + +func (pa *PluginAdapter) BroadcastCategoryBlockChange(teamID, userID string, blockCategory model.BlockCategoryWebsocketData) { + pa.logger.Debug( + "BroadcastCategoryBlockChange", + mlog.String("userID", userID), + mlog.String("teamID", teamID), + mlog.String("categoryID", blockCategory.CategoryID), + mlog.String("blockID", blockCategory.BlockID), + ) + + message := UpdateCategoryMessage{ + Action: websocketActionUpdateCategoryBlock, + TeamID: teamID, + BlockCategories: &blockCategory, + } + + pa.sendTeamMessage(websocketActionUpdateCategoryBlock, teamID, utils.StructToMap(message)) +} + +func (pa *PluginAdapter) BroadcastBlockDelete(teamID, blockID, boardID string) { now := utils.GetMillis() block := model.Block{} block.ID = blockID - block.ParentID = parentID + block.BoardID = boardID block.UpdateAt = now block.DeleteAt = now - block.WorkspaceID = workspaceID - pa.BroadcastBlockChange(workspaceID, block) + pa.BroadcastBlockChange(teamID, block) } -func (pa *PluginAdapter) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) { +func (pa *PluginAdapter) BroadcastBoardChange(teamID string, board *model.Board) { + pa.logger.Info("BroadcastingBoardChange", + mlog.String("teamID", teamID), + mlog.String("boardID", board.ID), + ) + + message := UpdateBoardMsg{ + Action: websocketActionUpdateBoard, + TeamID: teamID, + Board: board, + } + + pa.sendBoardMessage(teamID, board.ID, utils.StructToMap(message)) +} + +func (pa *PluginAdapter) BroadcastBoardDelete(teamID, boardID string) { + now := utils.GetMillis() + board := &model.Board{} + board.ID = boardID + board.TeamID = teamID + board.UpdateAt = now + board.DeleteAt = now + + pa.BroadcastBoardChange(teamID, board) +} + +func (pa *PluginAdapter) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) { + pa.logger.Info("BroadcastingMemberChange", + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + mlog.String("userID", member.UserID), + ) + + message := UpdateMemberMsg{ + Action: websocketActionUpdateMember, + TeamID: teamID, + Member: member, + } + + pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message)) +} + +func (pa *PluginAdapter) BroadcastMemberDelete(teamID, boardID, userID string) { + pa.logger.Info("BroadcastingMemberDelete", + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + mlog.String("userID", userID), + ) + + message := UpdateMemberMsg{ + Action: websocketActionDeleteMember, + TeamID: teamID, + Member: &model.BoardMember{UserID: userID, BoardID: boardID}, + } + + // when fetching the members of the board that should receive the + // member deletion message, the deleted member will not be one of + // them, so we need to ensure they receive the message + pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message), userID) +} + +func (pa *PluginAdapter) BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) { pa.logger.Debug("BroadcastingSubscriptionChange", - mlog.String("workspaceID", workspaceID), + mlog.String("TeamID", teamID), mlog.String("blockID", subscription.BlockID), mlog.String("subscriberID", subscription.SubscriberID), ) @@ -402,5 +581,5 @@ func (pa *PluginAdapter) BroadcastSubscriptionChange(workspaceID string, subscri Subscription: subscription, } - pa.sendWorkspaceMessage(websocketActionUpdateSubscription, workspaceID, utils.StructToMap(message)) + pa.sendTeamMessage(websocketActionUpdateSubscription, teamID, utils.StructToMap(message)) } diff --git a/server/ws/plugin_adapter_client.go b/server/ws/plugin_adapter_client.go index 6a7274d48..f13aaa875 100644 --- a/server/ws/plugin_adapter_client.go +++ b/server/ws/plugin_adapter_client.go @@ -12,7 +12,7 @@ type PluginAdapterClient struct { inactiveAt int64 webConnID string userID string - workspaces []string + teams []string blocks []string mu sync.RWMutex } @@ -25,24 +25,24 @@ func (pac *PluginAdapterClient) hasExpired(threshold time.Duration) bool { return !mmModel.GetTimeForMillis(atomic.LoadInt64(&pac.inactiveAt)).Add(threshold).After(time.Now()) } -func (pac *PluginAdapterClient) subscribeToWorkspace(workspaceID string) { +func (pac *PluginAdapterClient) subscribeToTeam(teamID string) { pac.mu.Lock() defer pac.mu.Unlock() - pac.workspaces = append(pac.workspaces, workspaceID) + pac.teams = append(pac.teams, teamID) } -func (pac *PluginAdapterClient) unsubscribeFromWorkspace(workspaceID string) { +func (pac *PluginAdapterClient) unsubscribeFromTeam(teamID string) { pac.mu.Lock() defer pac.mu.Unlock() - newClientWorkspaces := []string{} - for _, id := range pac.workspaces { - if id != workspaceID { - newClientWorkspaces = append(newClientWorkspaces, id) + newClientTeams := []string{} + for _, id := range pac.teams { + if id != teamID { + newClientTeams = append(newClientTeams, id) } } - pac.workspaces = newClientWorkspaces + pac.teams = newClientTeams } func (pac *PluginAdapterClient) unsubscribeFromBlock(blockID string) { @@ -58,12 +58,12 @@ func (pac *PluginAdapterClient) unsubscribeFromBlock(blockID string) { pac.blocks = newClientBlocks } -func (pac *PluginAdapterClient) isSubscribedToWorkspace(workspaceID string) bool { +func (pac *PluginAdapterClient) isSubscribedToTeam(teamID string) bool { pac.mu.RLock() defer pac.mu.RUnlock() - for _, id := range pac.workspaces { - if id == workspaceID { + for _, id := range pac.teams { + if id == teamID { return true } } diff --git a/server/ws/plugin_adapter_cluster.go b/server/ws/plugin_adapter_cluster.go index 9336eb528..2aa3cd31c 100644 --- a/server/ws/plugin_adapter_cluster.go +++ b/server/ws/plugin_adapter_cluster.go @@ -7,8 +7,10 @@ import ( ) type ClusterMessage struct { - WorkspaceID string + TeamID string + BoardID string Payload map[string]interface{} + EnsureUsers []string } func (pa *PluginAdapter) sendMessageToCluster(id string, clusterMessage *ClusterMessage) { @@ -46,6 +48,11 @@ func (pa *PluginAdapter) HandleClusterEvent(ev mmModel.PluginClusterEvent) { return } + if clusterMessage.BoardID != "" { + pa.sendBoardMessageSkipCluster(clusterMessage.TeamID, clusterMessage.BoardID, clusterMessage.Payload, clusterMessage.EnsureUsers...) + return + } + var action string if actionRaw, ok := clusterMessage.Payload["action"]; ok { if s, ok := actionRaw.(string); ok { @@ -54,12 +61,11 @@ func (pa *PluginAdapter) HandleClusterEvent(ev mmModel.PluginClusterEvent) { } if action == "" { // no action was specified in the event; assume block change and warn. - action = websocketActionUpdateBlock pa.api.LogWarn("cannot determine action from cluster message data", "id", ev.Id, "payload", clusterMessage.Payload, ) } - pa.sendWorkspaceMessageSkipCluster(action, clusterMessage.WorkspaceID, clusterMessage.Payload) + pa.sendTeamMessageSkipCluster(websocketActionUpdateBlock, clusterMessage.TeamID, clusterMessage.Payload) } diff --git a/server/ws/plugin_adapter_test.go b/server/ws/plugin_adapter_test.go index 01255949d..cbb65effc 100644 --- a/server/ws/plugin_adapter_test.go +++ b/server/ws/plugin_adapter_test.go @@ -4,22 +4,24 @@ import ( "sync" "testing" + "github.com/mattermost/focalboard/server/model" + mmModel "github.com/mattermost/mattermost-server/v6/model" "github.com/stretchr/testify/require" ) -func TestPluginAdapterWorkspaceSubscription(t *testing.T) { +func TestPluginAdapterTeamSubscription(t *testing.T) { th := SetupTestHelper(t) webConnID := mmModel.NewId() userID := mmModel.NewId() - workspaceID := mmModel.NewId() + teamID := mmModel.NewId() var pac *PluginAdapterClient t.Run("Should correctly add a connection", func(t *testing.T) { require.Empty(t, th.pa.listeners) - require.Empty(t, th.pa.listenersByWorkspace) + require.Empty(t, th.pa.listenersByTeam) th.pa.OnWebSocketConnect(webConnID, userID) require.Len(t, th.pa.listeners, 1) @@ -28,55 +30,55 @@ func TestPluginAdapterWorkspaceSubscription(t *testing.T) { require.True(t, ok) require.NotNil(t, pac) require.Equal(t, userID, pac.userID) - require.Empty(t, th.pa.listenersByWorkspace) + require.Empty(t, th.pa.listenersByTeam) }) - t.Run("Should correctly subscribe to a workspace", func(t *testing.T) { - require.False(t, pac.isSubscribedToWorkspace(workspaceID)) + t.Run("Should correctly subscribe to a team", func(t *testing.T) { + require.False(t, pac.isSubscribedToTeam(teamID)) - th.SubscribeWebConnToWorkspace(pac.webConnID, pac.userID, workspaceID) + th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID) - require.Len(t, th.pa.listenersByWorkspace[workspaceID], 1) - require.Contains(t, th.pa.listenersByWorkspace[workspaceID], pac) - require.Len(t, pac.workspaces, 1) - require.Contains(t, pac.workspaces, workspaceID) + require.Len(t, th.pa.listenersByTeam[teamID], 1) + require.Contains(t, th.pa.listenersByTeam[teamID], pac) + require.Len(t, pac.teams, 1) + require.Contains(t, pac.teams, teamID) - require.True(t, pac.isSubscribedToWorkspace(workspaceID)) + require.True(t, pac.isSubscribedToTeam(teamID)) }) - t.Run("Subscribing again to a subscribed workspace would have no effect", func(t *testing.T) { - require.True(t, pac.isSubscribedToWorkspace(workspaceID)) + t.Run("Subscribing again to a subscribed team would have no effect", func(t *testing.T) { + require.True(t, pac.isSubscribedToTeam(teamID)) - th.SubscribeWebConnToWorkspace(pac.webConnID, pac.userID, workspaceID) + th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID) - require.Len(t, th.pa.listenersByWorkspace[workspaceID], 1) - require.Contains(t, th.pa.listenersByWorkspace[workspaceID], pac) - require.Len(t, pac.workspaces, 1) - require.Contains(t, pac.workspaces, workspaceID) + require.Len(t, th.pa.listenersByTeam[teamID], 1) + require.Contains(t, th.pa.listenersByTeam[teamID], pac) + require.Len(t, pac.teams, 1) + require.Contains(t, pac.teams, teamID) - require.True(t, pac.isSubscribedToWorkspace(workspaceID)) + require.True(t, pac.isSubscribedToTeam(teamID)) }) - t.Run("Should correctly unsubscribe to a workspace", func(t *testing.T) { - require.True(t, pac.isSubscribedToWorkspace(workspaceID)) + t.Run("Should correctly unsubscribe to a team", func(t *testing.T) { + require.True(t, pac.isSubscribedToTeam(teamID)) - th.UnsubscribeWebConnFromWorkspace(pac.webConnID, pac.userID, workspaceID) + th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID) - require.Empty(t, th.pa.listenersByWorkspace[workspaceID]) - require.Empty(t, pac.workspaces) + require.Empty(t, th.pa.listenersByTeam[teamID]) + require.Empty(t, pac.teams) - require.False(t, pac.isSubscribedToWorkspace(workspaceID)) + require.False(t, pac.isSubscribedToTeam(teamID)) }) - t.Run("Unsubscribing again to an unsubscribed workspace would have no effect", func(t *testing.T) { - require.False(t, pac.isSubscribedToWorkspace(workspaceID)) + t.Run("Unsubscribing again to an unsubscribed team would have no effect", func(t *testing.T) { + require.False(t, pac.isSubscribedToTeam(teamID)) - th.UnsubscribeWebConnFromWorkspace(pac.webConnID, pac.userID, workspaceID) + th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID) - require.Empty(t, th.pa.listenersByWorkspace[workspaceID]) - require.Empty(t, pac.workspaces) + require.Empty(t, th.pa.listenersByTeam[teamID]) + require.Empty(t, pac.teams) - require.False(t, pac.isSubscribedToWorkspace(workspaceID)) + require.False(t, pac.isSubscribedToTeam(teamID)) }) t.Run("Should correctly be marked as inactive if disconnected", func(t *testing.T) { @@ -105,7 +107,7 @@ func TestPluginAdapterClientReconnect(t *testing.T) { webConnID := mmModel.NewId() userID := mmModel.NewId() - workspaceID := mmModel.NewId() + teamID := mmModel.NewId() var pac *PluginAdapterClient t.Run("A user should be able to reconnect within the accepted threshold and keep their subscriptions", func(t *testing.T) { @@ -120,8 +122,8 @@ func TestPluginAdapterClientReconnect(t *testing.T) { require.True(t, ok) require.NotNil(t, pac) - th.SubscribeWebConnToWorkspace(pac.webConnID, pac.userID, workspaceID) - require.True(t, pac.isSubscribedToWorkspace(workspaceID)) + th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID) + require.True(t, pac.isSubscribedToTeam(teamID)) // disconnect th.pa.OnWebSocketDisconnect(webConnID, userID) @@ -134,7 +136,7 @@ func TestPluginAdapterClientReconnect(t *testing.T) { require.Len(t, th.pa.listeners, 1) require.Len(t, th.pa.listenersByUserID[userID], 1) require.True(t, pac.isActive()) - require.True(t, pac.isSubscribedToWorkspace(workspaceID)) + require.True(t, pac.isSubscribedToTeam(teamID)) }) t.Run("Should remove old inactive connection when user connects with a different ID", func(t *testing.T) { @@ -162,7 +164,7 @@ func TestPluginAdapterClientReconnect(t *testing.T) { require.Len(t, th.pa.listenersByUserID[userID], 2) reconnectedPAC, ok := th.pa.listeners[webConnID] require.True(t, ok) - require.False(t, reconnectedPAC.isSubscribedToWorkspace(workspaceID)) + require.False(t, reconnectedPAC.isSubscribedToTeam(teamID)) }) t.Run("Should not remove active connections when user connects with a different ID", func(t *testing.T) { @@ -186,12 +188,12 @@ func TestPluginAdapterClientReconnect(t *testing.T) { }) } -func TestGetUserIDsForWorkspace(t *testing.T) { +func TestGetUserIDsForTeam(t *testing.T) { th := SetupTestHelper(t) - // we have two workspaces - workspaceID1 := mmModel.NewId() - workspaceID2 := mmModel.NewId() + // we have two teams + teamID1 := mmModel.NewId() + teamID2 := mmModel.NewId() // user 1 has two connections userID1 := mmModel.NewId() @@ -207,61 +209,162 @@ func TestGetUserIDsForWorkspace(t *testing.T) { go func(wg *sync.WaitGroup) { th.pa.OnWebSocketConnect(webConnID1, userID1) - th.SubscribeWebConnToWorkspace(webConnID1, userID1, workspaceID1) + th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1) wg.Done() }(wg) go func(wg *sync.WaitGroup) { th.pa.OnWebSocketConnect(webConnID2, userID1) - th.SubscribeWebConnToWorkspace(webConnID2, userID1, workspaceID2) + th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2) wg.Done() }(wg) go func(wg *sync.WaitGroup) { th.pa.OnWebSocketConnect(webConnID3, userID2) - th.SubscribeWebConnToWorkspace(webConnID3, userID2, workspaceID2) + th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2) wg.Done() }(wg) wg.Wait() - t.Run("should find that only user1 is connected to workspace 1", func(t *testing.T) { - userIDs := th.pa.getUserIDsForWorkspace(workspaceID1) + t.Run("should find that only user1 is connected to team 1", func(t *testing.T) { + userIDs := th.pa.getUserIDsForTeam(teamID1) require.ElementsMatch(t, []string{userID1}, userIDs) }) - t.Run("should find that both users are connected to workspace 2", func(t *testing.T) { - userIDs := th.pa.getUserIDsForWorkspace(workspaceID2) + t.Run("should find that both users are connected to team 2", func(t *testing.T) { + userIDs := th.pa.getUserIDsForTeam(teamID2) require.ElementsMatch(t, []string{userID1, userID2}, userIDs) }) - t.Run("should ignore user1 if webConn 2 inactive when getting workspace 2 user ids", func(t *testing.T) { + t.Run("should ignore user1 if webConn 2 inactive when getting team 2 user ids", func(t *testing.T) { th.pa.OnWebSocketDisconnect(webConnID2, userID1) - userIDs := th.pa.getUserIDsForWorkspace(workspaceID2) + userIDs := th.pa.getUserIDsForTeam(teamID2) require.ElementsMatch(t, []string{userID2}, userIDs) }) - t.Run("should still find user 1 in workspace 1 after the webConn 2 disconnection", func(t *testing.T) { - userIDs := th.pa.getUserIDsForWorkspace(workspaceID1) + t.Run("should still find user 1 in team 1 after the webConn 2 disconnection", func(t *testing.T) { + userIDs := th.pa.getUserIDsForTeam(teamID1) require.ElementsMatch(t, []string{userID1}, userIDs) }) t.Run("should find again both users if the webConn 2 comes back", func(t *testing.T) { th.pa.OnWebSocketConnect(webConnID2, userID1) - userIDs := th.pa.getUserIDsForWorkspace(workspaceID2) + userIDs := th.pa.getUserIDsForTeam(teamID2) require.ElementsMatch(t, []string{userID1, userID2}, userIDs) }) } +func TestGetUserIDsForTeamAndBoard(t *testing.T) { + th := SetupTestHelper(t) + + // we have two teams + teamID1 := mmModel.NewId() + boardID1 := mmModel.NewId() + teamID2 := mmModel.NewId() + boardID2 := mmModel.NewId() + + // user 1 has two connections + userID1 := mmModel.NewId() + webConnID1 := mmModel.NewId() + webConnID2 := mmModel.NewId() + + // user 2 has one connection + userID2 := mmModel.NewId() + webConnID3 := mmModel.NewId() + + wg := new(sync.WaitGroup) + wg.Add(3) + + go func(wg *sync.WaitGroup) { + th.pa.OnWebSocketConnect(webConnID1, userID1) + th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1) + wg.Done() + }(wg) + + go func(wg *sync.WaitGroup) { + th.pa.OnWebSocketConnect(webConnID2, userID1) + th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2) + wg.Done() + }(wg) + + go func(wg *sync.WaitGroup) { + th.pa.OnWebSocketConnect(webConnID3, userID2) + th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2) + wg.Done() + }(wg) + + wg.Wait() + + t.Run("should find that only user1 is connected to team 1 and board 1", func(t *testing.T) { + mockedMembers := []*model.BoardMember{{UserID: userID1}} + th.store.EXPECT(). + GetMembersForBoard(boardID1). + Return(mockedMembers, nil). + Times(1) + + userIDs := th.pa.getUserIDsForTeamAndBoard(teamID1, boardID1) + require.ElementsMatch(t, []string{userID1}, userIDs) + }) + + t.Run("should find that both users are connected to team 2 and board 2", func(t *testing.T) { + mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}} + th.store.EXPECT(). + GetMembersForBoard(boardID2). + Return(mockedMembers, nil). + Times(1) + + userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2) + require.ElementsMatch(t, []string{userID1, userID2}, userIDs) + }) + + t.Run("should find that only one user is connected to team 2 and board 2 if there is only one membership with both connected", func(t *testing.T) { + mockedMembers := []*model.BoardMember{{UserID: userID1}} + th.store.EXPECT(). + GetMembersForBoard(boardID2). + Return(mockedMembers, nil). + Times(1) + + userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2) + require.ElementsMatch(t, []string{userID1}, userIDs) + }) + + t.Run("should find only one if the other is inactive", func(t *testing.T) { + th.pa.OnWebSocketDisconnect(webConnID3, userID2) + defer th.pa.OnWebSocketConnect(webConnID3, userID2) + + mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}} + th.store.EXPECT(). + GetMembersForBoard(boardID2). + Return(mockedMembers, nil). + Times(1) + + userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2) + require.ElementsMatch(t, []string{userID1}, userIDs) + }) + + t.Run("should include a user that is not present if it's ensured", func(t *testing.T) { + userID3 := mmModel.NewId() + mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}} + th.store.EXPECT(). + GetMembersForBoard(boardID2). + Return(mockedMembers, nil). + Times(1) + + userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2, userID3) + require.ElementsMatch(t, []string{userID1, userID2, userID3}, userIDs) + }) +} + func TestParallelSubscriptionsOnMultipleConnections(t *testing.T) { th := SetupTestHelper(t) - workspaceID1 := mmModel.NewId() - workspaceID2 := mmModel.NewId() - workspaceID3 := mmModel.NewId() - workspaceID4 := mmModel.NewId() + teamID1 := mmModel.NewId() + teamID2 := mmModel.NewId() + teamID3 := mmModel.NewId() + teamID4 := mmModel.NewId() userID := mmModel.NewId() webConnID1 := mmModel.NewId() @@ -279,65 +382,65 @@ func TestParallelSubscriptionsOnMultipleConnections(t *testing.T) { wg.Add(4) go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToWorkspace(webConnID1, userID, workspaceID1) - require.True(t, pac1.isSubscribedToWorkspace(workspaceID1)) + th.SubscribeWebConnToTeam(webConnID1, userID, teamID1) + require.True(t, pac1.isSubscribedToTeam(teamID1)) - th.SubscribeWebConnToWorkspace(webConnID2, userID, workspaceID1) - require.True(t, pac2.isSubscribedToWorkspace(workspaceID1)) + th.SubscribeWebConnToTeam(webConnID2, userID, teamID1) + require.True(t, pac2.isSubscribedToTeam(teamID1)) - th.UnsubscribeWebConnFromWorkspace(webConnID1, userID, workspaceID1) - require.False(t, pac1.isSubscribedToWorkspace(workspaceID1)) + th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID1) + require.False(t, pac1.isSubscribedToTeam(teamID1)) - th.UnsubscribeWebConnFromWorkspace(webConnID2, userID, workspaceID1) - require.False(t, pac2.isSubscribedToWorkspace(workspaceID1)) + th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID1) + require.False(t, pac2.isSubscribedToTeam(teamID1)) wg.Done() }(wg) go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToWorkspace(webConnID1, userID, workspaceID2) - require.True(t, pac1.isSubscribedToWorkspace(workspaceID2)) + th.SubscribeWebConnToTeam(webConnID1, userID, teamID2) + require.True(t, pac1.isSubscribedToTeam(teamID2)) - th.SubscribeWebConnToWorkspace(webConnID2, userID, workspaceID2) - require.True(t, pac2.isSubscribedToWorkspace(workspaceID2)) + th.SubscribeWebConnToTeam(webConnID2, userID, teamID2) + require.True(t, pac2.isSubscribedToTeam(teamID2)) - th.UnsubscribeWebConnFromWorkspace(webConnID1, userID, workspaceID2) - require.False(t, pac1.isSubscribedToWorkspace(workspaceID2)) + th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID2) + require.False(t, pac1.isSubscribedToTeam(teamID2)) - th.UnsubscribeWebConnFromWorkspace(webConnID2, userID, workspaceID2) - require.False(t, pac2.isSubscribedToWorkspace(workspaceID2)) + th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID2) + require.False(t, pac2.isSubscribedToTeam(teamID2)) wg.Done() }(wg) go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToWorkspace(webConnID1, userID, workspaceID3) - require.True(t, pac1.isSubscribedToWorkspace(workspaceID3)) + th.SubscribeWebConnToTeam(webConnID1, userID, teamID3) + require.True(t, pac1.isSubscribedToTeam(teamID3)) - th.SubscribeWebConnToWorkspace(webConnID2, userID, workspaceID3) - require.True(t, pac2.isSubscribedToWorkspace(workspaceID3)) + th.SubscribeWebConnToTeam(webConnID2, userID, teamID3) + require.True(t, pac2.isSubscribedToTeam(teamID3)) - th.UnsubscribeWebConnFromWorkspace(webConnID1, userID, workspaceID3) - require.False(t, pac1.isSubscribedToWorkspace(workspaceID3)) + th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID3) + require.False(t, pac1.isSubscribedToTeam(teamID3)) - th.UnsubscribeWebConnFromWorkspace(webConnID2, userID, workspaceID3) - require.False(t, pac2.isSubscribedToWorkspace(workspaceID3)) + th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID3) + require.False(t, pac2.isSubscribedToTeam(teamID3)) wg.Done() }(wg) go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToWorkspace(webConnID1, userID, workspaceID4) - require.True(t, pac1.isSubscribedToWorkspace(workspaceID4)) + th.SubscribeWebConnToTeam(webConnID1, userID, teamID4) + require.True(t, pac1.isSubscribedToTeam(teamID4)) - th.SubscribeWebConnToWorkspace(webConnID2, userID, workspaceID4) - require.True(t, pac2.isSubscribedToWorkspace(workspaceID4)) + th.SubscribeWebConnToTeam(webConnID2, userID, teamID4) + require.True(t, pac2.isSubscribedToTeam(teamID4)) - th.UnsubscribeWebConnFromWorkspace(webConnID1, userID, workspaceID4) - require.False(t, pac1.isSubscribedToWorkspace(workspaceID4)) + th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID4) + require.False(t, pac1.isSubscribedToTeam(teamID4)) - th.UnsubscribeWebConnFromWorkspace(webConnID2, userID, workspaceID4) - require.False(t, pac2.isSubscribedToWorkspace(workspaceID4)) + th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID4) + require.False(t, pac2.isSubscribedToTeam(teamID4)) wg.Done() }(wg) diff --git a/server/ws/server.go b/server/ws/server.go index 89c5b92ec..40ad45a5d 100644 --- a/server/ws/server.go +++ b/server/ws/server.go @@ -9,7 +9,6 @@ import ( "github.com/gorilla/websocket" "github.com/mattermost/focalboard/server/auth" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -17,23 +16,16 @@ import ( const singleUserID = "single-user-id" -type wsClient struct { - *websocket.Conn - mu sync.Mutex - workspaces []string - blocks []string -} - -func (c *wsClient) WriteJSON(v interface{}) error { - c.mu.Lock() - defer c.mu.Unlock() - err := c.Conn.WriteJSON(v) +func (wss *websocketSession) WriteJSON(v interface{}) error { + wss.mu.Lock() + defer wss.mu.Unlock() + err := wss.conn.WriteJSON(v) return err } -func (c *wsClient) isSubscribedToWorkspace(workspaceID string) bool { - for _, id := range c.workspaces { - if id == workspaceID { +func (wss *websocketSession) isSubscribedToTeam(teamID string) bool { + for _, id := range wss.teams { + if id == teamID { return true } } @@ -41,8 +33,8 @@ func (c *wsClient) isSubscribedToWorkspace(workspaceID string) bool { return false } -func (c *wsClient) isSubscribedToBlock(blockID string) bool { - for _, id := range c.blocks { +func (wss *websocketSession) isSubscribedToBlock(blockID string) bool { + for _, id := range wss.blocks { if id == blockID { return true } @@ -53,15 +45,16 @@ func (c *wsClient) isSubscribedToBlock(blockID string) bool { // Server is a WebSocket server. type Server struct { - upgrader websocket.Upgrader - listeners map[*wsClient]bool - listenersByWorkspace map[string][]*wsClient - listenersByBlock map[string][]*wsClient - mu sync.RWMutex - auth *auth.Auth - singleUserToken string - isMattermostAuth bool - logger *mlog.Logger + upgrader websocket.Upgrader + listeners map[*websocketSession]bool + listenersByTeam map[string][]*websocketSession + listenersByBlock map[string][]*websocketSession + mu sync.RWMutex + auth *auth.Auth + singleUserToken string + isMattermostAuth bool + logger *mlog.Logger + store Store } // UpdateClientConfig is sent on block updates. @@ -71,8 +64,11 @@ type UpdateClientConfig struct { } type websocketSession struct { - client *wsClient + conn *websocket.Conn userID string + mu sync.Mutex + teams []string + blocks []string } func (wss *websocketSession) isAuthenticated() bool { @@ -80,11 +76,11 @@ func (wss *websocketSession) isAuthenticated() bool { } // NewServer creates a new Server. -func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool, logger *mlog.Logger) *Server { +func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool, logger *mlog.Logger, store Store) *Server { return &Server{ - listeners: make(map[*wsClient]bool), - listenersByWorkspace: make(map[string][]*wsClient), - listenersByBlock: make(map[string][]*wsClient), + listeners: make(map[*websocketSession]bool), + listenersByTeam: make(map[string][]*websocketSession), + listenersByBlock: make(map[string][]*websocketSession), upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true @@ -94,6 +90,7 @@ func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool, l singleUserToken: singleUserToken, isMattermostAuth: isMattermostAuth, logger: logger, + store: store, } } @@ -111,35 +108,38 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { } // create an empty session with websocket client - wsSession := websocketSession{ - client: &wsClient{client, sync.Mutex{}, []string{}, []string{}}, + wsSession := &websocketSession{ + conn: client, userID: "", + mu: sync.Mutex{}, + teams: []string{}, + blocks: []string{}, } if ws.isMattermostAuth { wsSession.userID = r.Header.Get("Mattermost-User-Id") } - ws.addListener(wsSession.client) + ws.addListener(wsSession) // Make sure we close the connection when the function returns defer func() { - ws.logger.Debug("DISCONNECT WebSocket", mlog.Stringer("client", wsSession.client.RemoteAddr())) + ws.logger.Debug("DISCONNECT WebSocket", mlog.Stringer("client", wsSession.conn.RemoteAddr())) - // Remove client from listeners - ws.removeListener(wsSession.client) - wsSession.client.Close() + // Remove session from listeners + ws.removeListener(wsSession) + wsSession.conn.Close() }() // Simple message handling loop for { - _, p, err := wsSession.client.ReadMessage() + _, p, err := wsSession.conn.ReadMessage() if err != nil { ws.logger.Error("ERROR WebSocket", - mlog.Stringer("client", wsSession.client.RemoteAddr()), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), mlog.Err(err), ) - ws.removeListener(wsSession.client) + ws.removeListener(wsSession) break } @@ -154,8 +154,8 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { } if command.Action == websocketActionAuth { - ws.logger.Debug(`Command: AUTH`, mlog.Stringer("client", wsSession.client.RemoteAddr())) - ws.authenticateListener(&wsSession, command.Token) + ws.logger.Debug(`Command: AUTH`, mlog.Stringer("client", wsSession.conn.RemoteAddr())) + ws.authenticateListener(wsSession, command.Token) continue } @@ -165,13 +165,13 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { // authentication if command.Action == websocketActionSubscribeBlocks { ws.logger.Debug(`Command: SUBSCRIBE_BLOCKS`, - mlog.String("workspaceID", command.WorkspaceID), - mlog.Stringer("client", wsSession.client.RemoteAddr()), + mlog.String("teamID", command.TeamID), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), ) if !ws.isCommandReadTokenValid(command) { ws.logger.Error(`Rejected invalid read token`, - mlog.Stringer("client", wsSession.client.RemoteAddr()), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), mlog.String("action", command.Action), mlog.String("readToken", command.ReadToken), ) @@ -179,19 +179,19 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { continue } - ws.subscribeListenerToBlocks(wsSession.client, command.BlockIDs) + ws.subscribeListenerToBlocks(wsSession, command.BlockIDs) continue } if command.Action == websocketActionUnsubscribeBlocks { ws.logger.Debug(`Command: UNSUBSCRIBE_BLOCKS`, - mlog.String("workspaceID", command.WorkspaceID), - mlog.Stringer("client", wsSession.client.RemoteAddr()), + mlog.String("teamID", command.TeamID), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), ) if !ws.isCommandReadTokenValid(command) { ws.logger.Error(`Rejected invalid read token`, - mlog.Stringer("client", wsSession.client.RemoteAddr()), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), mlog.String("action", command.Action), mlog.String("readToken", command.ReadToken), ) @@ -199,7 +199,7 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { continue } - ws.unsubscribeListenerFromBlocks(wsSession.client, command.BlockIDs) + ws.unsubscribeListenerFromBlocks(wsSession, command.BlockIDs) continue } @@ -207,7 +207,7 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { // not be processed if !wsSession.isAuthenticated() { ws.logger.Error(`Rejected unauthenticated message`, - mlog.Stringer("client", wsSession.client.RemoteAddr()), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), mlog.String("action", command.Action), ) @@ -215,10 +215,10 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { } switch command.Action { - case websocketActionSubscribeWorkspace: - ws.logger.Debug(`Command: SUBSCRIBE_WORKSPACE`, - mlog.String("workspaceID", command.WorkspaceID), - mlog.Stringer("client", wsSession.client.RemoteAddr()), + case websocketActionSubscribeTeam: + ws.logger.Debug(`Command: SUBSCRIBE_TEAM`, + mlog.String("teamID", command.TeamID), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), ) // if single user mode, check that the userID is valid and @@ -229,21 +229,23 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { } // if not in single user mode validate that the session - // has permissions to the workspace + // has permissions to the team } else { - if !ws.auth.DoesUserHaveWorkspaceAccess(wsSession.userID, command.WorkspaceID) { + ws.logger.Debug("Not single user mode") + if !ws.auth.DoesUserHaveTeamAccess(wsSession.userID, command.TeamID) { + ws.logger.Error("WS user doesn't have team access", mlog.String("teamID", command.TeamID), mlog.String("userID", wsSession.userID)) continue } } - ws.subscribeListenerToWorkspace(wsSession.client, command.WorkspaceID) - case websocketActionUnsubscribeWorkspace: - ws.logger.Debug(`Command: UNSUBSCRIBE_WORKSPACE`, - mlog.String("workspaceID", command.WorkspaceID), - mlog.Stringer("client", wsSession.client.RemoteAddr()), + ws.subscribeListenerToTeam(wsSession, command.TeamID) + case websocketActionUnsubscribeTeam: + ws.logger.Debug(`Command: UNSUBSCRIBE_TEAM`, + mlog.String("teamID", command.TeamID), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), ) - ws.unsubscribeListenerFromWorkspace(wsSession.client, command.WorkspaceID) + ws.unsubscribeListenerFromTeam(wsSession, command.TeamID) default: ws.logger.Error(`ERROR webSocket command, invalid action`, mlog.String("action", command.Action)) } @@ -253,157 +255,172 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { // isCommandReadTokenValid ensures that a command contains a read // token and a set of block ids that said token is valid for. func (ws *Server) isCommandReadTokenValid(command WebsocketCommand) bool { - if len(command.WorkspaceID) == 0 { + if len(command.TeamID) == 0 { return false } - container := store.Container{WorkspaceID: command.WorkspaceID} - - if len(command.ReadToken) != 0 && len(command.BlockIDs) != 0 { - // Read token must be valid for all block IDs - for _, blockID := range command.BlockIDs { - isValid, _ := ws.auth.IsValidReadToken(container, blockID, command.ReadToken) - if !isValid { - return false - } + boardID := "" + // all the blocks must be part of the same board + for _, blockID := range command.BlockIDs { + block, err := ws.store.GetBlock(blockID) + if err != nil { + return false + } + + if boardID == "" { + boardID = block.BoardID + continue + } + + if boardID != block.BoardID { + return false } - return true } - return false + // the read token must be valid for the board + isValid, err := ws.auth.IsValidReadToken(boardID, command.ReadToken) + if err != nil { + ws.logger.Error(`ERROR when checking token validity`, + mlog.String("teamID", command.TeamID), + mlog.Err(err), + ) + return false + } + + return isValid } // addListener adds a listener to the websocket server. The listener // should not receive any update from the server until it subscribes // itself to some entity changes. Adding a listener to the server // doesn't mean that it's authenticated in any way. -func (ws *Server) addListener(client *wsClient) { +func (ws *Server) addListener(listener *websocketSession) { ws.mu.Lock() defer ws.mu.Unlock() - ws.listeners[client] = true + ws.listeners[listener] = true } // removeListener removes a listener and all its subscriptions, if // any, from the websockets server. -func (ws *Server) removeListener(client *wsClient) { +func (ws *Server) removeListener(listener *websocketSession) { ws.mu.Lock() defer ws.mu.Unlock() // remove the listener from its subscriptions, if any - // workspace subscriptions - for _, workspace := range client.workspaces { - ws.removeListenerFromWorkspace(client, workspace) + // team subscriptions + for _, team := range listener.teams { + ws.removeListenerFromTeam(listener, team) } // block subscriptions - for _, block := range client.blocks { - ws.removeListenerFromBlock(client, block) + for _, block := range listener.blocks { + ws.removeListenerFromBlock(listener, block) } - delete(ws.listeners, client) + delete(ws.listeners, listener) } -// subscribeListenerToWorkspace safely modifies the listener and the -// server to subscribe the listener to a given workspace updates. -func (ws *Server) subscribeListenerToWorkspace(client *wsClient, workspaceID string) { - if client.isSubscribedToWorkspace(workspaceID) { +// subscribeListenerToTeam safely modifies the listener and the +// server to subscribe the listener to a given team updates. +func (ws *Server) subscribeListenerToTeam(listener *websocketSession, teamID string) { + if listener.isSubscribedToTeam(teamID) { return } ws.mu.Lock() defer ws.mu.Unlock() - ws.listenersByWorkspace[workspaceID] = append(ws.listenersByWorkspace[workspaceID], client) - client.workspaces = append(client.workspaces, workspaceID) + ws.listenersByTeam[teamID] = append(ws.listenersByTeam[teamID], listener) + listener.teams = append(listener.teams, teamID) } -// unsubscribeListenerFromWorkspace safely modifies the listener and +// unsubscribeListenerFromTeam safely modifies the listener and // the server data structures to remove the link between the listener -// and a given workspace ID. -func (ws *Server) unsubscribeListenerFromWorkspace(client *wsClient, workspaceID string) { - if !client.isSubscribedToWorkspace(workspaceID) { +// and a given team ID. +func (ws *Server) unsubscribeListenerFromTeam(listener *websocketSession, teamID string) { + if !listener.isSubscribedToTeam(teamID) { return } ws.mu.Lock() defer ws.mu.Unlock() - ws.removeListenerFromWorkspace(client, workspaceID) + ws.removeListenerFromTeam(listener, teamID) } // subscribeListenerToBlocks safely modifies the listener and the // server to subscribe the listener to a given set of block updates. -func (ws *Server) subscribeListenerToBlocks(client *wsClient, blockIDs []string) { +func (ws *Server) subscribeListenerToBlocks(listener *websocketSession, blockIDs []string) { ws.mu.Lock() defer ws.mu.Unlock() for _, blockID := range blockIDs { - if client.isSubscribedToBlock(blockID) { + if listener.isSubscribedToBlock(blockID) { continue } - ws.listenersByBlock[blockID] = append(ws.listenersByBlock[blockID], client) - client.blocks = append(client.blocks, blockID) + ws.listenersByBlock[blockID] = append(ws.listenersByBlock[blockID], listener) + listener.blocks = append(listener.blocks, blockID) } } // unsubscribeListenerFromBlocks safely modifies the listener and the // server data structures to remove the link between the listener and // a given set of block IDs. -func (ws *Server) unsubscribeListenerFromBlocks(client *wsClient, blockIDs []string) { +func (ws *Server) unsubscribeListenerFromBlocks(listener *websocketSession, blockIDs []string) { ws.mu.Lock() defer ws.mu.Unlock() for _, blockID := range blockIDs { - if client.isSubscribedToBlock(blockID) { - ws.removeListenerFromBlock(client, blockID) + if listener.isSubscribedToBlock(blockID) { + ws.removeListenerFromBlock(listener, blockID) } } } -// removeListenerFromWorkspace removes the listener from both its own -// block subscribed list and the server listeners by workspace map. -func (ws *Server) removeListenerFromWorkspace(client *wsClient, workspaceID string) { - // we remove the listener from the workspace index - newWorkspaceListeners := []*wsClient{} - for _, listener := range ws.listenersByWorkspace[workspaceID] { - if listener != client { - newWorkspaceListeners = append(newWorkspaceListeners, listener) +// removeListenerFromTeam removes the listener from both its own +// block subscribed list and the server listeners by team map. +func (ws *Server) removeListenerFromTeam(listener *websocketSession, teamID string) { + // we remove the listener from the team index + newTeamListeners := []*websocketSession{} + for _, l := range ws.listenersByTeam[teamID] { + if l != listener { + newTeamListeners = append(newTeamListeners, l) } } - ws.listenersByWorkspace[workspaceID] = newWorkspaceListeners + ws.listenersByTeam[teamID] = newTeamListeners - // we remove the workspace from the listener subscription list - newClientWorkspaces := []string{} - for _, id := range client.workspaces { - if id != workspaceID { - newClientWorkspaces = append(newClientWorkspaces, id) + // we remove the team from the listener subscription list + newListenerTeams := []string{} + for _, id := range listener.teams { + if id != teamID { + newListenerTeams = append(newListenerTeams, id) } } - client.workspaces = newClientWorkspaces + listener.teams = newListenerTeams } // removeListenerFromBlock removes the listener from both its own // block subscribed list and the server listeners by block map. -func (ws *Server) removeListenerFromBlock(client *wsClient, blockID string) { +func (ws *Server) removeListenerFromBlock(listener *websocketSession, blockID string) { // we remove the listener from the block index - newBlockListeners := []*wsClient{} - for _, listener := range ws.listenersByBlock[blockID] { - if listener != client { - newBlockListeners = append(newBlockListeners, listener) + newBlockListeners := []*websocketSession{} + for _, l := range ws.listenersByBlock[blockID] { + if l != listener { + newBlockListeners = append(newBlockListeners, l) } } ws.listenersByBlock[blockID] = newBlockListeners // we remove the block from the listener subscription list - newClientBlocks := []string{} - for _, id := range client.blocks { + newListenerBlocks := []string{} + for _, id := range listener.blocks { if id != blockID { - newClientBlocks = append(newClientBlocks, id) + newListenerBlocks = append(newListenerBlocks, id) } } - client.blocks = newClientBlocks + listener.blocks = newListenerBlocks } func (ws *Server) getUserIDForToken(token string) string { @@ -433,7 +450,7 @@ func (ws *Server) authenticateListener(wsSession *websocketSession, token string ws.logger.Debug( "authenticateListener: Ignoring already authenticated session", mlog.String("userID", wsSession.userID), - mlog.Stringer("client", wsSession.client.RemoteAddr()), + mlog.Stringer("client", wsSession.conn.RemoteAddr()), ) return } @@ -441,74 +458,172 @@ func (ws *Server) authenticateListener(wsSession *websocketSession, token string // Authenticate session userID := ws.getUserIDForToken(token) if userID == "" { - wsSession.client.Close() + wsSession.conn.Close() return } // Authenticated wsSession.userID = userID - ws.logger.Debug("authenticateListener: Authenticated", mlog.String("userID", userID), mlog.Stringer("client", wsSession.client.RemoteAddr())) + ws.logger.Debug("authenticateListener: Authenticated", mlog.String("userID", userID), mlog.Stringer("client", wsSession.conn.RemoteAddr())) } // getListenersForBlock returns the listeners subscribed to a // block changes. -func (ws *Server) getListenersForBlock(blockID string) []*wsClient { +func (ws *Server) getListenersForBlock(blockID string) []*websocketSession { return ws.listenersByBlock[blockID] } -// getListenersForWorkspace returns the listeners subscribed to a -// workspace changes. -func (ws *Server) getListenersForWorkspace(workspaceID string) []*wsClient { - return ws.listenersByWorkspace[workspaceID] +// getListenersForTeam returns the listeners subscribed to a +// team changes. +func (ws *Server) getListenersForTeam(teamID string) []*websocketSession { + return ws.listenersByTeam[teamID] +} + +// getListenersForTeamAndBoard returns the listeners subscribed to a +// team changes and members of a given board. +func (ws *Server) getListenersForTeamAndBoard(teamID, boardID string, ensureUsers ...string) []*websocketSession { + members, err := ws.store.GetMembersForBoard(boardID) + if err != nil { + ws.logger.Error("error getting members for board", + mlog.String("method", "getListenersForTeamAndBoard"), + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + ) + return nil + } + + memberMap := map[string]bool{} + for _, member := range members { + memberMap[member.UserID] = true + } + for _, id := range ensureUsers { + memberMap[id] = true + } + + memberIDs := []string{} + for id := range memberMap { + memberIDs = append(memberIDs, id) + } + + listeners := []*websocketSession{} + for _, memberID := range memberIDs { + for _, listener := range ws.listenersByTeam[teamID] { + if listener.userID == memberID { + listeners = append(listeners, listener) + } + } + } + return listeners } // BroadcastBlockDelete broadcasts delete messages to clients. -func (ws *Server) BroadcastBlockDelete(workspaceID, blockID, parentID string) { +func (ws *Server) BroadcastBlockDelete(teamID, blockID, boardID string) { now := utils.GetMillis() block := model.Block{} block.ID = blockID - block.ParentID = parentID + block.BoardID = boardID block.UpdateAt = now block.DeleteAt = now - block.WorkspaceID = workspaceID - ws.BroadcastBlockChange(workspaceID, block) + ws.BroadcastBlockChange(teamID, block) } // BroadcastBlockChange broadcasts update messages to clients. -func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) { +func (ws *Server) BroadcastBlockChange(teamID string, block model.Block) { blockIDsToNotify := []string{block.ID, block.ParentID} - message := UpdateMsg{ + message := UpdateBlockMsg{ Action: websocketActionUpdateBlock, + TeamID: teamID, Block: block, } - listeners := ws.getListenersForWorkspace(workspaceID) - ws.logger.Debug("listener(s) for workspaceID", + listeners := ws.getListenersForTeamAndBoard(teamID, block.BoardID) + ws.logger.Trace("listener(s) for teamID", mlog.Int("listener_count", len(listeners)), - mlog.String("workspaceID", workspaceID), + mlog.String("teamID", teamID), + mlog.String("boardID", block.BoardID), ) for _, blockID := range blockIDsToNotify { listeners = append(listeners, ws.getListenersForBlock(blockID)...) - ws.logger.Debug("listener(s) for blockID", + ws.logger.Trace("listener(s) for blockID", mlog.Int("listener_count", len(listeners)), mlog.String("blockID", blockID), ) } for _, listener := range listeners { - ws.logger.Debug("Broadcast change", - mlog.String("workspaceID", workspaceID), + ws.logger.Debug("Broadcast block change", + mlog.String("teamID", teamID), mlog.String("blockID", block.ID), - mlog.Stringer("remoteAddr", listener.RemoteAddr()), + mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), ) err := listener.WriteJSON(message) if err != nil { ws.logger.Error("broadcast error", mlog.Err(err)) - listener.Close() + listener.conn.Close() + } + } +} + +func (ws *Server) BroadcastCategoryChange(category model.Category) { + message := UpdateCategoryMessage{ + Action: websocketActionUpdateCategory, + TeamID: category.TeamID, + Category: &category, + } + + listeners := ws.getListenersForTeam(category.TeamID) + ws.logger.Debug("listener(s) for teamID", + mlog.Int("listener_count", len(listeners)), + mlog.String("teamID", category.TeamID), + mlog.String("categoryID", category.ID), + ) + + for _, listener := range listeners { + ws.logger.Debug("Broadcast block change", + mlog.Int("listener_count", len(listeners)), + mlog.String("teamID", category.TeamID), + mlog.String("categoryID", category.ID), + mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), + ) + + if err := listener.WriteJSON(message); err != nil { + ws.logger.Error("broadcast category change error", mlog.Err(err)) + listener.conn.Close() + } + } +} + +func (ws *Server) BroadcastCategoryBlockChange(teamID, userID string, blockCategory model.BlockCategoryWebsocketData) { + message := UpdateCategoryMessage{ + Action: websocketActionUpdateCategoryBlock, + TeamID: teamID, + BlockCategories: &blockCategory, + } + + listeners := ws.getListenersForTeam(teamID) + ws.logger.Debug("listener(s) for teamID", + mlog.Int("listener_count", len(listeners)), + mlog.String("teamID", teamID), + mlog.String("categoryID", blockCategory.CategoryID), + mlog.String("blockID", blockCategory.BlockID), + ) + + for _, listener := range listeners { + ws.logger.Debug("Broadcast block change", + mlog.Int("listener_count", len(listeners)), + mlog.String("teamID", teamID), + mlog.String("categoryID", blockCategory.CategoryID), + mlog.String("blockID", blockCategory.BlockID), + mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), + ) + + if err := listener.WriteJSON(message); err != nil { + ws.logger.Error("broadcast category change error", mlog.Err(err)) + listener.conn.Close() } } } @@ -527,12 +642,113 @@ func (ws *Server) BroadcastConfigChange(clientConfig model.ClientConfig) { for listener := range listeners { ws.logger.Debug("Broadcast Config change", - mlog.Stringer("remoteAddr", listener.RemoteAddr()), + mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), ) err := listener.WriteJSON(message) if err != nil { ws.logger.Error("broadcast error", mlog.Err(err)) - listener.Close() + listener.conn.Close() + } + } +} + +func (ws *Server) BroadcastBoardChange(teamID string, board *model.Board) { + message := UpdateBoardMsg{ + Action: websocketActionUpdateBoard, + TeamID: teamID, + Board: board, + } + + listeners := ws.getListenersForTeamAndBoard(teamID, board.ID) + ws.logger.Trace("listener(s) for teamID and boardID", + mlog.Int("listener_count", len(listeners)), + mlog.String("teamID", teamID), + mlog.String("boardID", board.ID), + ) + + for _, listener := range listeners { + ws.logger.Debug("Broadcast board change", + mlog.String("teamID", teamID), + mlog.String("boardID", board.ID), + mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), + ) + + err := listener.WriteJSON(message) + if err != nil { + ws.logger.Error("broadcast error", mlog.Err(err)) + listener.conn.Close() + } + } +} + +func (ws *Server) BroadcastBoardDelete(teamID, boardID string) { + now := utils.GetMillis() + board := &model.Board{} + board.ID = boardID + board.TeamID = teamID + board.UpdateAt = now + board.DeleteAt = now + + ws.BroadcastBoardChange(teamID, board) +} + +func (ws *Server) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) { + message := UpdateMemberMsg{ + Action: websocketActionUpdateMember, + TeamID: teamID, + Member: member, + } + + listeners := ws.getListenersForTeamAndBoard(teamID, boardID) + ws.logger.Trace("listener(s) for teamID and boardID", + mlog.Int("listener_count", len(listeners)), + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + ) + + for _, listener := range listeners { + ws.logger.Debug("Broadcast member change", + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), + ) + + err := listener.WriteJSON(message) + if err != nil { + ws.logger.Error("broadcast error", mlog.Err(err)) + listener.conn.Close() + } + } +} + +func (ws *Server) BroadcastMemberDelete(teamID, boardID, userID string) { + message := UpdateMemberMsg{ + Action: websocketActionDeleteMember, + TeamID: teamID, + Member: &model.BoardMember{UserID: userID, BoardID: boardID}, + } + + // when fetching the members of the board that should receive the + // member deletion message, the deleted member will not be one of + // them, so we need to ensure they receive the message + listeners := ws.getListenersForTeamAndBoard(teamID, boardID, userID) + ws.logger.Trace("listener(s) for teamID and boardID", + mlog.Int("listener_count", len(listeners)), + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + ) + + for _, listener := range listeners { + ws.logger.Debug("Broadcast member removal", + mlog.String("teamID", teamID), + mlog.String("boardID", boardID), + mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), + ) + + err := listener.WriteJSON(message) + if err != nil { + ws.logger.Error("broadcast error", mlog.Err(err)) + listener.conn.Close() } } } diff --git a/server/ws/server_test.go b/server/ws/server_test.go index 55d884fe1..d0ac3455b 100644 --- a/server/ws/server_test.go +++ b/server/ws/server_test.go @@ -12,199 +12,207 @@ import ( "github.com/stretchr/testify/require" ) -func TestWorkspaceSubscription(t *testing.T) { - server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}) - client := &wsClient{&websocket.Conn{}, sync.Mutex{}, []string{}, []string{}} - session := &websocketSession{client: client} - workspaceID := "fake-workspace-id" +func TestTeamSubscription(t *testing.T) { + server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil) + session := &websocketSession{ + conn: &websocket.Conn{}, + mu: sync.Mutex{}, + teams: []string{}, + blocks: []string{}, + } + teamID := "fake-team-id" t.Run("Should correctly add a session", func(t *testing.T) { - server.addListener(session.client) + server.addListener(session) require.Len(t, server.listeners, 1) - require.Empty(t, server.listenersByWorkspace) - require.Empty(t, client.workspaces) + require.Empty(t, server.listenersByTeam) + require.Empty(t, session.teams) }) - t.Run("Should correctly subscribe to a workspace", func(t *testing.T) { - require.False(t, client.isSubscribedToWorkspace(workspaceID)) + t.Run("Should correctly subscribe to a team", func(t *testing.T) { + require.False(t, session.isSubscribedToTeam(teamID)) - server.subscribeListenerToWorkspace(client, workspaceID) + server.subscribeListenerToTeam(session, teamID) - require.Len(t, server.listenersByWorkspace[workspaceID], 1) - require.Contains(t, server.listenersByWorkspace[workspaceID], client) - require.Len(t, client.workspaces, 1) - require.Contains(t, client.workspaces, workspaceID) + require.Len(t, server.listenersByTeam[teamID], 1) + require.Contains(t, server.listenersByTeam[teamID], session) + require.Len(t, session.teams, 1) + require.Contains(t, session.teams, teamID) - require.True(t, client.isSubscribedToWorkspace(workspaceID)) + require.True(t, session.isSubscribedToTeam(teamID)) }) - t.Run("Subscribing again to a subscribed workspace would have no effect", func(t *testing.T) { - require.True(t, client.isSubscribedToWorkspace(workspaceID)) + t.Run("Subscribing again to a subscribed team would have no effect", func(t *testing.T) { + require.True(t, session.isSubscribedToTeam(teamID)) - server.subscribeListenerToWorkspace(client, workspaceID) + server.subscribeListenerToTeam(session, teamID) - require.Len(t, server.listenersByWorkspace[workspaceID], 1) - require.Contains(t, server.listenersByWorkspace[workspaceID], client) - require.Len(t, client.workspaces, 1) - require.Contains(t, client.workspaces, workspaceID) + require.Len(t, server.listenersByTeam[teamID], 1) + require.Contains(t, server.listenersByTeam[teamID], session) + require.Len(t, session.teams, 1) + require.Contains(t, session.teams, teamID) - require.True(t, client.isSubscribedToWorkspace(workspaceID)) + require.True(t, session.isSubscribedToTeam(teamID)) }) - t.Run("Should correctly unsubscribe to a workspace", func(t *testing.T) { - require.True(t, client.isSubscribedToWorkspace(workspaceID)) + t.Run("Should correctly unsubscribe to a team", func(t *testing.T) { + require.True(t, session.isSubscribedToTeam(teamID)) - server.unsubscribeListenerFromWorkspace(client, workspaceID) + server.unsubscribeListenerFromTeam(session, teamID) - require.Empty(t, server.listenersByWorkspace[workspaceID]) - require.Empty(t, client.workspaces) + require.Empty(t, server.listenersByTeam[teamID]) + require.Empty(t, session.teams) - require.False(t, client.isSubscribedToWorkspace(workspaceID)) + require.False(t, session.isSubscribedToTeam(teamID)) }) - t.Run("Unsubscribing again to an unsubscribed workspace would have no effect", func(t *testing.T) { - require.False(t, client.isSubscribedToWorkspace(workspaceID)) + t.Run("Unsubscribing again to an unsubscribed team would have no effect", func(t *testing.T) { + require.False(t, session.isSubscribedToTeam(teamID)) - server.unsubscribeListenerFromWorkspace(client, workspaceID) + server.unsubscribeListenerFromTeam(session, teamID) - require.Empty(t, server.listenersByWorkspace[workspaceID]) - require.Empty(t, client.workspaces) + require.Empty(t, server.listenersByTeam[teamID]) + require.Empty(t, session.teams) - require.False(t, client.isSubscribedToWorkspace(workspaceID)) + require.False(t, session.isSubscribedToTeam(teamID)) }) t.Run("Should correctly be removed from the server", func(t *testing.T) { - server.removeListener(client) + server.removeListener(session) require.Empty(t, server.listeners) }) - t.Run("If subscribed to workspaces and removed, should be removed from the workspaces subscription list", func(t *testing.T) { - workspaceID2 := "other-fake-workspace-id" + t.Run("If subscribed to teams and removed, should be removed from the teams subscription list", func(t *testing.T) { + teamID2 := "other-fake-team-id" - server.addListener(session.client) - server.subscribeListenerToWorkspace(client, workspaceID) - server.subscribeListenerToWorkspace(client, workspaceID2) + server.addListener(session) + server.subscribeListenerToTeam(session, teamID) + server.subscribeListenerToTeam(session, teamID2) require.Len(t, server.listeners, 1) - require.Contains(t, server.listenersByWorkspace[workspaceID], client) - require.Contains(t, server.listenersByWorkspace[workspaceID2], client) + require.Contains(t, server.listenersByTeam[teamID], session) + require.Contains(t, server.listenersByTeam[teamID2], session) - server.removeListener(client) + server.removeListener(session) require.Empty(t, server.listeners) - require.Empty(t, server.listenersByWorkspace[workspaceID]) - require.Empty(t, server.listenersByWorkspace[workspaceID2]) + require.Empty(t, server.listenersByTeam[teamID]) + require.Empty(t, server.listenersByTeam[teamID2]) }) } func TestBlocksSubscription(t *testing.T) { - server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}) - client := &wsClient{&websocket.Conn{}, sync.Mutex{}, []string{}, []string{}} - session := &websocketSession{client: client} + server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil) + session := &websocketSession{ + conn: &websocket.Conn{}, + mu: sync.Mutex{}, + teams: []string{}, + blocks: []string{}, + } blockID1 := "block1" blockID2 := "block2" blockID3 := "block3" blockIDs := []string{blockID1, blockID2, blockID3} t.Run("Should correctly add a session", func(t *testing.T) { - server.addListener(session.client) + server.addListener(session) require.Len(t, server.listeners, 1) - require.Empty(t, server.listenersByWorkspace) - require.Empty(t, client.workspaces) + require.Empty(t, server.listenersByTeam) + require.Empty(t, session.teams) }) t.Run("Should correctly subscribe to a set of blocks", func(t *testing.T) { - require.False(t, client.isSubscribedToBlock(blockID1)) - require.False(t, client.isSubscribedToBlock(blockID2)) - require.False(t, client.isSubscribedToBlock(blockID3)) + require.False(t, session.isSubscribedToBlock(blockID1)) + require.False(t, session.isSubscribedToBlock(blockID2)) + require.False(t, session.isSubscribedToBlock(blockID3)) - server.subscribeListenerToBlocks(client, blockIDs) + server.subscribeListenerToBlocks(session, blockIDs) require.Len(t, server.listenersByBlock[blockID1], 1) - require.Contains(t, server.listenersByBlock[blockID1], client) + require.Contains(t, server.listenersByBlock[blockID1], session) require.Len(t, server.listenersByBlock[blockID2], 1) - require.Contains(t, server.listenersByBlock[blockID2], client) + require.Contains(t, server.listenersByBlock[blockID2], session) require.Len(t, server.listenersByBlock[blockID3], 1) - require.Contains(t, server.listenersByBlock[blockID3], client) - require.Len(t, client.blocks, 3) - require.ElementsMatch(t, blockIDs, client.blocks) + require.Contains(t, server.listenersByBlock[blockID3], session) + require.Len(t, session.blocks, 3) + require.ElementsMatch(t, blockIDs, session.blocks) - require.True(t, client.isSubscribedToBlock(blockID1)) - require.True(t, client.isSubscribedToBlock(blockID2)) - require.True(t, client.isSubscribedToBlock(blockID3)) + require.True(t, session.isSubscribedToBlock(blockID1)) + require.True(t, session.isSubscribedToBlock(blockID2)) + require.True(t, session.isSubscribedToBlock(blockID3)) t.Run("Subscribing again to a subscribed block would have no effect", func(t *testing.T) { - require.True(t, client.isSubscribedToBlock(blockID1)) - require.True(t, client.isSubscribedToBlock(blockID2)) - require.True(t, client.isSubscribedToBlock(blockID3)) + require.True(t, session.isSubscribedToBlock(blockID1)) + require.True(t, session.isSubscribedToBlock(blockID2)) + require.True(t, session.isSubscribedToBlock(blockID3)) - server.subscribeListenerToBlocks(client, blockIDs) + server.subscribeListenerToBlocks(session, blockIDs) require.Len(t, server.listenersByBlock[blockID1], 1) - require.Contains(t, server.listenersByBlock[blockID1], client) + require.Contains(t, server.listenersByBlock[blockID1], session) require.Len(t, server.listenersByBlock[blockID2], 1) - require.Contains(t, server.listenersByBlock[blockID2], client) + require.Contains(t, server.listenersByBlock[blockID2], session) require.Len(t, server.listenersByBlock[blockID3], 1) - require.Contains(t, server.listenersByBlock[blockID3], client) - require.Len(t, client.blocks, 3) - require.ElementsMatch(t, blockIDs, client.blocks) + require.Contains(t, server.listenersByBlock[blockID3], session) + require.Len(t, session.blocks, 3) + require.ElementsMatch(t, blockIDs, session.blocks) - require.True(t, client.isSubscribedToBlock(blockID1)) - require.True(t, client.isSubscribedToBlock(blockID2)) - require.True(t, client.isSubscribedToBlock(blockID3)) + require.True(t, session.isSubscribedToBlock(blockID1)) + require.True(t, session.isSubscribedToBlock(blockID2)) + require.True(t, session.isSubscribedToBlock(blockID3)) }) }) t.Run("Should correctly unsubscribe to a set of blocks", func(t *testing.T) { - require.True(t, client.isSubscribedToBlock(blockID1)) - require.True(t, client.isSubscribedToBlock(blockID2)) - require.True(t, client.isSubscribedToBlock(blockID3)) + require.True(t, session.isSubscribedToBlock(blockID1)) + require.True(t, session.isSubscribedToBlock(blockID2)) + require.True(t, session.isSubscribedToBlock(blockID3)) - server.unsubscribeListenerFromBlocks(client, blockIDs) + server.unsubscribeListenerFromBlocks(session, blockIDs) require.Empty(t, server.listenersByBlock[blockID1]) require.Empty(t, server.listenersByBlock[blockID2]) require.Empty(t, server.listenersByBlock[blockID3]) - require.Empty(t, client.blocks) + require.Empty(t, session.blocks) - require.False(t, client.isSubscribedToBlock(blockID1)) - require.False(t, client.isSubscribedToBlock(blockID2)) - require.False(t, client.isSubscribedToBlock(blockID3)) + require.False(t, session.isSubscribedToBlock(blockID1)) + require.False(t, session.isSubscribedToBlock(blockID2)) + require.False(t, session.isSubscribedToBlock(blockID3)) }) t.Run("Unsubscribing again to an unsubscribed block would have no effect", func(t *testing.T) { - require.False(t, client.isSubscribedToBlock(blockID1)) + require.False(t, session.isSubscribedToBlock(blockID1)) - server.unsubscribeListenerFromBlocks(client, []string{blockID1}) + server.unsubscribeListenerFromBlocks(session, []string{blockID1}) require.Empty(t, server.listenersByBlock[blockID1]) - require.Empty(t, client.blocks) + require.Empty(t, session.blocks) - require.False(t, client.isSubscribedToBlock(blockID1)) + require.False(t, session.isSubscribedToBlock(blockID1)) }) t.Run("Should correctly be removed from the server", func(t *testing.T) { - server.removeListener(client) + server.removeListener(session) require.Empty(t, server.listeners) }) t.Run("If subscribed to blocks and removed, should be removed from the blocks subscription list", func(t *testing.T) { - server.addListener(session.client) - server.subscribeListenerToBlocks(client, blockIDs) + server.addListener(session) + server.subscribeListenerToBlocks(session, blockIDs) require.Len(t, server.listeners, 1) require.Len(t, server.listenersByBlock[blockID1], 1) - require.Contains(t, server.listenersByBlock[blockID1], client) + require.Contains(t, server.listenersByBlock[blockID1], session) require.Len(t, server.listenersByBlock[blockID2], 1) - require.Contains(t, server.listenersByBlock[blockID2], client) + require.Contains(t, server.listenersByBlock[blockID2], session) require.Len(t, server.listenersByBlock[blockID3], 1) - require.Contains(t, server.listenersByBlock[blockID3], client) - require.Len(t, client.blocks, 3) - require.ElementsMatch(t, blockIDs, client.blocks) + require.Contains(t, server.listenersByBlock[blockID3], session) + require.Len(t, session.blocks, 3) + require.ElementsMatch(t, blockIDs, session.blocks) - server.removeListener(client) + server.removeListener(session) require.Empty(t, server.listeners) require.Empty(t, server.listenersByBlock[blockID1]) @@ -215,7 +223,7 @@ func TestBlocksSubscription(t *testing.T) { func TestGetUserIDForTokenInSingleUserMode(t *testing.T) { singleUserToken := "single-user-token" - server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}) + server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil) server.singleUserToken = singleUserToken t.Run("Should return nothing if the token is empty", func(t *testing.T) { diff --git a/webapp/cypress/integration/createBoard.ts b/webapp/cypress/integration/createBoard.ts index 530709a6d..75c2416df 100644 --- a/webapp/cypress/integration/createBoard.ts +++ b/webapp/cypress/integration/createBoard.ts @@ -99,7 +99,6 @@ describe('Create and delete board / card', () => { cy.log('**Create table view**') cy.get('.ViewHeader').get('.DropdownIcon').first().parent().click() cy.get('.ViewHeader').contains('Add view').click() - cy.get('.ViewHeader').contains('Add view').click() cy.get('.ViewHeader'). contains('Add view'). parent(). @@ -131,7 +130,6 @@ describe('Create and delete board / card', () => { cy.get('.Sidebar .octo-sidebar-list'). contains(boardTitle). parent(). - parent(). find('.MenuWrapper'). find('button.IconButton'). click({force: true}) @@ -143,6 +141,7 @@ describe('Create and delete board / card', () => { it('MM-T4433 Scrolls the kanban board when dragging card to edge', () => { // Visit a page and create new empty board cy.visit('/') + cy.wait(500) cy.uiCreateEmptyBoard() // Create 10 empty groups diff --git a/webapp/cypress/integration/loginActions.ts b/webapp/cypress/integration/loginActions.ts index 6fcff310e..34c05578c 100644 --- a/webapp/cypress/integration/loginActions.ts +++ b/webapp/cypress/integration/loginActions.ts @@ -29,8 +29,6 @@ describe('Login actions', () => { cy.get('#login-username').type(username) cy.get('#login-password').type(password) cy.get('button').contains('Register').click() - cy.location('pathname', {timeout: 10000}).should('include', '/welcome') - cy.get('a').contains('No thanks').click() workspaceIsAvailable() // Can log out user @@ -98,8 +96,6 @@ describe('Login actions', () => { cy.get('#login-username').type('new-user') cy.get('#login-password').type('new-password') cy.get('button').contains('Register').click() - cy.location('pathname', {timeout: 10000}).should('include', '/welcome') - cy.get('a').contains('No thanks').click() workspaceIsAvailable() }) }) diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 0cdb9ddb8..844f6858b 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -7,6 +7,9 @@ "BoardComponent.no-property": "No {property}", "BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column cannot be removed.", "BoardComponent.show": "Show", + "BoardMember.schemeAdmin": "Admin", + "BoardMember.schemeEditor": "Editor", + "BoardMember.schemeNone": "None", "BoardPage.newVersion": "A new version of Boards is available, click here to reload.", "BoardPage.syncFailed": "Board may be deleted or access revoked.", "BoardTemplateSelector.add-template": "New template", @@ -14,10 +17,11 @@ "BoardTemplateSelector.delete-template": "Delete", "BoardTemplateSelector.description": "Choose a template to help you get started. Easily customize the template to fit your needs, or create an empty board to start from scratch.", "BoardTemplateSelector.edit-template": "Edit", - "BoardTemplateSelector.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of \"{workspaceName}\" will have access to boards created here.", - "BoardTemplateSelector.plugin.no-content-title": "Create a Board in {workspaceName}", + "BoardTemplateSelector.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of \"{teamName}\" will have access to boards created here.", + "BoardTemplateSelector.plugin.no-content-title": "Create a Board in {teamName}", "BoardTemplateSelector.title": "Create a Board", "BoardTemplateSelector.use-this-template": "Use this template", + "BoardsSwitcher.Title": "Find Boards", "BoardsUnfurl.Remainder": "+{remainder} more", "BoardsUnfurl.Updated": "Updated {time}", "Calculations.Options.average.displayName": "Average", @@ -81,6 +85,10 @@ "CardDialog.delete-confirmation-dialog-heading": "Confirm card delete!", "CardDialog.editing-template": "You're editing a template.", "CardDialog.nocard": "This card doesn't exist or is inaccessible.", + "Categories.CreateCategoryDialog.CancelText": "Cancel", + "Categories.CreateCategoryDialog.CreateText": "Create", + "Categories.CreateCategoryDialog.Placeholder": "Name your category", + "Categories.CreateCategoryDialog.UpdateText": "Update", "CenterPanel.Share": "Share", "ColorOption.selectColor": "Select {color} Color", "Comment.delete": "Delete", @@ -101,11 +109,6 @@ "ContentBlock.moveDown": "Move down", "ContentBlock.moveUp": "Move up", "ContentBlock.text": "text", - "DashboardPage.CenterPanel.ChangeChannels": "Use the switcher to easily change channels", - "DashboardPage.CenterPanel.NoWorkspaces": "Sorry, we could not find any channels matching that term", - "DashboardPage.CenterPanel.NoWorkspacesDescription": "Please try searching for another term", - "DashboardPage.showEmpty": "Show empty", - "DashboardPage.title": "Dashboard", "DeleteBoardDialog.confirm-cancel": "Cancel", "DeleteBoardDialog.confirm-delete": "Delete", "DeleteBoardDialog.confirm-info": "Are you sure you want to delete the board “{boardTitle}”? Deleting it will delete all cards in the board.", @@ -121,6 +124,11 @@ "Filter.not-includes": "doesn't include", "FilterComponent.add-filter": "+ Add filter", "FilterComponent.delete": "Delete", + "FindBoFindBoardsDialog.IntroText": "Search for boards", + "FindBoardsDialog.NoResultsFor": "No results for \"{searchQuery}\"", + "FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.", + "FindBoardsDialog.SubTitle": "Type to find a board. Use UP/DOWN to browse. ENTER to select, ESC to dismiss", + "FindBoardsDialog.Title": "Find Boards", "GalleryCard.copiedLink": "Copied!", "GalleryCard.copyLink": "Copy link", "GalleryCard.delete": "Delete", @@ -134,9 +142,7 @@ "KanbanCard.delete": "Delete", "KanbanCard.duplicate": "Duplicate", "KanbanCard.untitled": "Untitled", - "Mutator.duplicate-board": "duplicate board", "Mutator.new-card-from-template": "new card from template", - "Mutator.new-template-from-board": "new template from board", "Mutator.new-template-from-card": "new template from card", "PropertyMenu.Delete": "Delete", "PropertyMenu.changeType": "Change property type", @@ -171,26 +177,31 @@ "ShareBoard.copiedLink": "Copied!", "ShareBoard.copyLink": "Copy link", "ShareBoard.regenerate": "Regenerate token", + "ShareBoard.teamPermissionsText": "Everyone at {teamName} Team", "ShareBoard.tokenRegenrated": "Token regenerated", + "ShareBoard.userPermissionsRemoveMemberText": "Remove member", + "ShareBoard.userPermissionsYouText": "(You)", "Sidebar.about": "About Focalboard", "Sidebar.add-board": "+ Add board", "Sidebar.changePassword": "Change password", "Sidebar.delete-board": "Delete board", - "Sidebar.duplicate-board": "Duplicate board", "Sidebar.export-archive": "Export archive", "Sidebar.import": "Import", "Sidebar.import-archive": "Import archive", "Sidebar.invite-users": "Invite users", "Sidebar.logout": "Log out", - "Sidebar.no-more-workspaces": "No more workspaces", - "Sidebar.no-views-in-board": "No pages inside", + "Sidebar.no-boards-in-category": "No boards inside", "Sidebar.random-icons": "Random icons", "Sidebar.set-language": "Set language", "Sidebar.set-theme": "Set theme", "Sidebar.settings": "Settings", - "Sidebar.template-from-board": "New template from board", "Sidebar.untitled-board": "(Untitled Board)", - "Sidebar.untitled-view": "(Untitled View)", + "SidebarCategories.BlocksMenu.Move": "Move To...", + "SidebarCategories.CategoryMenu.CreateNew": "Create New Category", + "SidebarCategories.CategoryMenu.Delete": "Delete Category", + "SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in {categoryName} will move back to the Boards categories. You're not removed from any boards.", + "SidebarCategories.CategoryMenu.DeleteModal.Title": "Delete this category?", + "SidebarCategories.CategoryMenu.Update": "Rename Category", "TableComponent.add-icon": "Add icon", "TableComponent.name": "Name", "TableComponent.plus-new": "+ New", @@ -286,5 +297,6 @@ "generic.previous": "Previous", "tutorial_tip.seen": "Seen this before?", "tutorial_tip.out": "Opt out of these tips", - "register.signup-title": "Sign up for your account" + "register.signup-title": "Sign up for your account", + "shareBoard.lastAdmin": "Boards must have at least one Administrator" } diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 256b3d28f..a4f42d148 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -1,45 +1,26 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useMemo} from 'react' -import { - Router, - Redirect, - Route, - Switch, -} from 'react-router-dom' +import React, {useEffect} from 'react' import {IntlProvider} from 'react-intl' import {DndProvider} from 'react-dnd' import {HTML5Backend} from 'react-dnd-html5-backend' import {TouchBackend} from 'react-dnd-touch-backend' -import {createBrowserHistory, History} from 'history' +import {History} from 'history' import TelemetryClient from './telemetry/telemetryClient' -import {IAppWindow} from './types' import {getMessages} from './i18n' import {FlashMessages} from './components/flashMessages' import NewVersionBanner from './components/newVersionBanner' -import BoardPage from './pages/boardPage' -import ChangePasswordPage from './pages/changePasswordPage' -import DashboardPage from './pages/dashboard/dashboardPage' -import WelcomePage from './pages/welcome/welcomePage' -import ErrorPage from './pages/errorPage' -import LoginPage from './pages/loginPage' -import RegisterPage from './pages/registerPage' import {Utils} from './utils' import wsClient from './wsclient' -import {fetchMe, getLoggedIn, getMe} from './store/users' +import {fetchMe, getMe} from './store/users' import {getLanguage, fetchLanguage} from './store/language' -import {setGlobalError, getGlobalError} from './store/globalError' import {useAppSelector, useAppDispatch} from './store/hooks' import {fetchClientConfig} from './store/clientConfig' +import FocalboardRouter from './router' -import {IUser, UserPropPrefix} from './user' -import {UserSettingKey, UserSettings} from './userSettings' - -declare let window: IAppWindow - -const UUID_REGEX = new RegExp(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/) +import {IUser} from './user' type Props = { history?: History @@ -47,34 +28,15 @@ type Props = { const App = (props: Props): JSX.Element => { const language = useAppSelector(getLanguage) - const loggedIn = useAppSelector(getLoggedIn) - const globalError = useAppSelector(getGlobalError) const me = useAppSelector(getMe) const dispatch = useAppDispatch() - let browserHistory: History - if (props.history) { - browserHistory = props.history - } else { - browserHistory = useMemo(() => { - return createBrowserHistory({basename: Utils.getFrontendBaseURL()}) - }, []) - } - useEffect(() => { dispatch(fetchLanguage()) dispatch(fetchMe()) dispatch(fetchClientConfig()) }, []) - if (Utils.isFocalboardPlugin()) { - useEffect(() => { - if (window.frontendBaseURL) { - browserHistory.replace(window.location.pathname.replace(window.frontendBaseURL, '')) - } - }, []) - } - // this is a temporary solution while we're using legacy routes // for shared boards as a way to disable websockets, and should be // removed when anonymous plugin routes are implemented. This @@ -95,16 +57,6 @@ const App = (props: Props): JSX.Element => { } }, [me]) - let globalErrorRedirect = null - if (globalError) { - globalErrorRedirect = - setTimeout(() => dispatch(setGlobalError('')), 0) - } - - const continueToWelcomeScreen = () => { - return (me?.id !== 'single-user') && loggedIn === true && !(me?.props && me?.props[UserPropPrefix + UserSettingKey.WelcomePageViewed]) - } - return ( { > - -
-
- - - {globalErrorRedirect} - { - Utils.isFocalboardPlugin() && - { - if (loggedIn === false) { - return - } - - if (continueToWelcomeScreen()) { - return - } - - if (Utils.isFocalboardPlugin() && UserSettings.lastWorkspaceId) { - return - } - - if (loggedIn === true) { - return - } - - return null - }} - /> - } - - - - - - - - - - - - - - - - - { - if (loggedIn === false) { - return - } - - if (continueToWelcomeScreen()) { - const originalPath = `/board/${Utils.buildOriginalPath('', boardId, viewId, cardId)}` - return - } - - if (loggedIn === true) { - return - } - - return null - }} - /> - - - - { - const originalPath = `/workspace/${Utils.buildOriginalPath(workspaceId, boardId, viewId, cardId)}` - if (loggedIn === false) { - let redirectUrl = '/' + Utils.buildURL(originalPath) - if (redirectUrl.indexOf('//') === 0) { - redirectUrl = redirectUrl.slice(1) - } - const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}` - return - } else if (loggedIn === true) { - if (continueToWelcomeScreen()) { - return - } - - return ( - - ) - } - return null - }} - /> - - - - - - - - {!Utils.isFocalboardPlugin() && - { - // Since these 3 path values are optional and they can be anything, we can pass /x/y/z and it will - // match this route however these values may not be valid so we should at the very least check - // board id for descisions made below - const boardIdIsValidUUIDV4 = UUID_REGEX.test(boardId || '') - - if (loggedIn === false) { - return - } - - if (continueToWelcomeScreen()) { - const originalPath = `/${Utils.buildOriginalPath('', boardId, viewId, cardId)}` - const queryString = boardIdIsValidUUIDV4 ? `r=${originalPath}` : '' - return - } - - if (loggedIn === true) { - return - } - - return null - }} - />} - -
+
+
+ +
- +
) diff --git a/webapp/src/archiver.ts b/webapp/src/archiver.ts index bb29bf17f..5d57cf444 100644 --- a/webapp/src/archiver.ts +++ b/webapp/src/archiver.ts @@ -54,7 +54,7 @@ class Archiver { } static isValidBlock(block: Block): boolean { - if (!block.id || !block.rootId) { + if (!block.id || !block.boardId) { return false } diff --git a/webapp/src/blocks/__snapshots__/board.test.ts.snap b/webapp/src/blocks/__snapshots__/board.test.ts.snap new file mode 100644 index 000000000..2d8a52994 --- /dev/null +++ b/webapp/src/blocks/__snapshots__/board.test.ts.snap @@ -0,0 +1,312 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`board tests correctly generate patches for boards and blocks should add fields on update and remove it in the undo 1`] = ` +Array [ + Object { + "blockIDs": Array [ + "test-old-block-id", + ], + "blockPatches": Array [ + Object { + "deletedFields": Array [], + "updatedFields": Object { + "newField": "new field", + }, + }, + ], + "boardIDs": Array [ + "test-board-id", + ], + "boardPatches": Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + ], + }, + Object { + "blockIDs": Array [ + "test-old-block-id", + ], + "blockPatches": Array [ + Object { + "deletedFields": Array [ + "newField", + ], + "updatedFields": Object {}, + }, + ], + "boardIDs": Array [ + "test-board-id", + ], + "boardPatches": Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + ], + }, +] +`; + +exports[`board tests correctly generate patches for boards and blocks should generate two empty patches for the same board and block 1`] = ` +Array [ + Object { + "blockIDs": Array [ + "test-card-id", + ], + "blockPatches": Array [ + Object { + "deletedFields": Array [], + "updatedFields": Object {}, + }, + ], + "boardIDs": Array [ + "test-board-id", + ], + "boardPatches": Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + ], + }, + Object { + "blockIDs": Array [ + "test-card-id", + ], + "blockPatches": Array [ + Object { + "deletedFields": Array [], + "updatedFields": Object {}, + }, + ], + "boardIDs": Array [ + "test-board-id", + ], + "boardPatches": Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + ], + }, +] +`; + +exports[`board tests correctly generate patches from two boards should add card properties on the redo and remove them on the undo 1`] = ` +Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [ + Object { + "id": "new-property-id", + "name": "property-name", + "options": Array [ + Object { + "color": "propColorYellow", + "id": "opt", + "value": "val", + }, + ], + "type": "select", + }, + ], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + Object { + "deletedCardProperties": Array [ + "new-property-id", + ], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, +] +`; + +exports[`board tests correctly generate patches from two boards should add card properties on the redo and undo if they exists in both, but differ 1`] = ` +Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [ + Object { + "id": "new-property-id", + "name": "property-name", + "options": Array [ + Object { + "color": "propColorYellow", + "id": "opt", + "value": "val", + }, + ], + "type": "select", + }, + ], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [ + Object { + "id": "new-property-id", + "name": "a-different-name", + "options": Array [ + Object { + "color": "propColorYellow", + "id": "opt", + "value": "val", + }, + ], + "type": "select", + }, + ], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, +] +`; + +exports[`board tests correctly generate patches from two boards should add card properties on the redo and undo if they exists in both, but their options are different 1`] = ` +Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [ + Object { + "id": "new-property-id", + "name": "property-name", + "options": Array [ + Object { + "color": "propColorYellow", + "id": "opt", + "value": "val", + }, + ], + "type": "select", + }, + ], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [ + Object { + "id": "new-property-id", + "name": "property-name", + "options": Array [ + Object { + "color": "propColorBrown", + "id": "another-opt", + "value": "val", + }, + ], + "type": "select", + }, + ], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, +] +`; + +exports[`board tests correctly generate patches from two boards should add properties on the update patch and remove them on the undo 1`] = ` +Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object { + "prop1": "val1", + }, + }, + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [ + "prop1", + ], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, +] +`; + +exports[`board tests correctly generate patches from two boards should generate two empty patches for the same board 1`] = ` +Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object {}, + "updatedProperties": Object {}, + }, +] +`; + +exports[`board tests correctly generate patches from two boards should update a column calculation on the redo and revert it on the undo 1`] = ` +Array [ + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object { + "cal1": "val1", + }, + "updatedProperties": Object {}, + }, + Object { + "deletedCardProperties": Array [], + "deletedColumnCalculations": Array [], + "deletedProperties": Array [], + "updatedCardProperties": Array [], + "updatedColumnCalculations": Object { + "cal1": "another-val1", + }, + "updatedProperties": Object {}, + }, +] +`; diff --git a/webapp/src/blocks/block.ts b/webapp/src/blocks/block.ts index 414b8fb76..3024708ab 100644 --- a/webapp/src/blocks/block.ts +++ b/webapp/src/blocks/block.ts @@ -6,14 +6,15 @@ import difference from 'lodash/difference' import {Utils} from '../utils' const contentBlockTypes = ['text', 'image', 'divider', 'checkbox'] as const + +// ToDo: remove type board const blockTypes = [...contentBlockTypes, 'board', 'view', 'card', 'comment', 'unknown'] as const type ContentBlockTypes = typeof contentBlockTypes[number] type BlockTypes = typeof blockTypes[number] interface BlockPatch { - workspaceId?: string + boardId?: string parentId?: string - rootId?: string schema?: number type?: BlockTypes title?: string @@ -25,9 +26,8 @@ interface BlockPatch { interface Block { id: string - workspaceId: string + boardId: string parentId: string - rootId: string createdBy: string modifiedBy: string @@ -47,9 +47,8 @@ function createBlock(block?: Block): Block { return { id: block?.id || Utils.createGuid(Utils.blockTypeToIDType(block?.type)), schema: 1, - workspaceId: block?.workspaceId || '', + boardId: block?.boardId || '', parentId: block?.parentId || '', - rootId: block?.rootId || '', createdBy: block?.createdBy || '', modifiedBy: block?.modifiedBy || '', type: block?.type || 'unknown', @@ -61,7 +60,7 @@ function createBlock(block?: Block): Block { } } -// createPatchesFromBlock creates two BlockPatch instances, one that +// createPatchesFromBlocks creates two BlockPatch instances, one that // contains the delta to update the block and another one for the undo // action, in case it happens function createPatchesFromBlocks(newBlock: Block, oldBlock: Block): BlockPatch[] { diff --git a/webapp/src/blocks/board.test.ts b/webapp/src/blocks/board.test.ts new file mode 100644 index 000000000..908b19ff4 --- /dev/null +++ b/webapp/src/blocks/board.test.ts @@ -0,0 +1,134 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {TestBlockFactory} from '../test/testBlockFactory' + +import {createPatchesFromBoards, createBoard, IPropertyTemplate, createPatchesFromBoardsAndBlocks} from './board' +import {createBlock} from './block' + +describe('board tests', () => { + describe('correctly generate patches from two boards', () => { + it('should generate two empty patches for the same board', () => { + const board = TestBlockFactory.createBoard() + const result = createPatchesFromBoards(board, board) + expect(result).toMatchSnapshot() + }) + + it('should add properties on the update patch and remove them on the undo', () => { + const board = TestBlockFactory.createBoard() + board.properties = { + prop1: 'val1', + prop2: 'val2', + } + const oldBoard = createBoard(board) + oldBoard.properties = { + prop2: 'val2', + } + + const result = createPatchesFromBoards(board, oldBoard) + expect(result).toMatchSnapshot() + }) + + it('should add card properties on the redo and remove them on the undo', () => { + const board = TestBlockFactory.createBoard() + const oldBoard = createBoard(board) + board.cardProperties.push({ + id: 'new-property-id', + name: 'property-name', + type: 'select', + options: [{ + id: 'opt', + value: 'val', + color: 'propColorYellow', + }], + }) + + const result = createPatchesFromBoards(board, oldBoard) + expect(result).toMatchSnapshot() + }) + + it('should add card properties on the redo and undo if they exists in both, but differ', () => { + const cardProperty = { + id: 'new-property-id', + name: 'property-name', + type: 'select', + options: [{ + id: 'opt', + value: 'val', + color: 'propColorYellow', + }], + } as IPropertyTemplate + + const board = TestBlockFactory.createBoard() + const oldBoard = createBoard(board) + board.cardProperties = [cardProperty] + oldBoard.cardProperties = [{...cardProperty, name: 'a-different-name'}] + + const result = createPatchesFromBoards(board, oldBoard) + expect(result).toMatchSnapshot() + }) + + it('should add card properties on the redo and undo if they exists in both, but their options are different', () => { + const cardProperty = { + id: 'new-property-id', + name: 'property-name', + type: 'select', + options: [{ + id: 'opt', + value: 'val', + color: 'propColorYellow', + }], + } as IPropertyTemplate + + const board = TestBlockFactory.createBoard() + const oldBoard = createBoard(board) + board.cardProperties = [cardProperty] + oldBoard.cardProperties = [{ + ...cardProperty, + options: [{ + id: 'another-opt', + value: 'val', + color: 'propColorBrown', + }], + }] + + const result = createPatchesFromBoards(board, oldBoard) + expect(result).toMatchSnapshot() + }) + + it('should update a column calculation on the redo and revert it on the undo', () => { + const board = TestBlockFactory.createBoard() + const oldBoard = createBoard(board) + board.columnCalculations = { + cal1: 'val1', + } + oldBoard.columnCalculations = { + cal1: 'another-val1', + } + + const result = createPatchesFromBoards(board, oldBoard) + expect(result).toMatchSnapshot() + }) + }) + + describe('correctly generate patches for boards and blocks', () => { + const board = TestBlockFactory.createBoard() + board.id = 'test-board-id' + const card = TestBlockFactory.createCard() + card.id = 'test-card-id' + + it('should generate two empty patches for the same board and block', () => { + const result = createPatchesFromBoardsAndBlocks(board, board, [card.id], [card], [card]) + expect(result).toMatchSnapshot() + }) + + it('should add fields on update and remove it in the undo', () => { + const oldBlock = TestBlockFactory.createText(card) + oldBlock.id = 'test-old-block-id' + const newBlock = createBlock(oldBlock) + newBlock.fields.newField = 'new field' + + const result = createPatchesFromBoardsAndBlocks(board, board, [newBlock.id], [newBlock], [oldBlock]) + expect(result).toMatchSnapshot() + }) + }) +}) diff --git a/webapp/src/blocks/board.ts b/webapp/src/blocks/board.ts index 8eb844f4d..7dc68d511 100644 --- a/webapp/src/blocks/board.ts +++ b/webapp/src/blocks/board.ts @@ -1,10 +1,80 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. + +import difference from 'lodash/difference' + import {Utils, IDType} from '../utils' -import {Block, createBlock} from './block' +import {Block, BlockPatch, createPatchesFromBlocks} from './block' import {Card} from './card' +const BoardTypeOpen = 'O' +const BoardTypePrivate = 'P' +const boardTypes = [BoardTypeOpen, BoardTypePrivate] +type BoardTypes = typeof boardTypes[number] + +type Board = { + id: string + teamId: string + channelId?: string + createdBy: string + modifiedBy: string + type: BoardTypes + + title: string + description: string + icon?: string + showDescription: boolean + isTemplate: boolean + templateVersion: number + properties: Record + cardProperties: IPropertyTemplate[] + columnCalculations: Record + + createAt: number + updateAt: number + deleteAt: number +} + +type BoardPatch = { + type?: BoardTypes + title?: string + description?: string + icon?: string + showDescription?: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updatedProperties?: Record + deletedProperties?: string[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updatedCardProperties?: IPropertyTemplate[] + deletedCardProperties?: string[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updatedColumnCalculations?: Record + deletedColumnCalculations?: string[] +} + +type BoardMember = { + boardId: string + userId: string + roles?: string + schemeAdmin: boolean + schemeEditor: boolean + schemeCommenter: boolean + schemeViewer: boolean +} + +type BoardsAndBlocks = { + boards: Board[], + blocks: Block[], +} + +type BoardsAndBlocksPatch = { + boardIDs: string[], + boardPatches: BoardPatch[], + blockIDs: string[], + blockPatches: BlockPatch[], +} + type PropertyType = 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'person' | 'file' | 'checkbox' | 'url' | 'email' | 'phone' | 'createdTime' | 'createdBy' | 'updatedTime' | 'updatedBy' interface IPropertyOption { @@ -21,20 +91,8 @@ interface IPropertyTemplate { options: IPropertyOption[] } -type BoardFields = { - icon: string - description: string - showDescription?: boolean - isTemplate?: boolean - cardProperties: IPropertyTemplate[] - columnCalculations: Record -} - -type Board = Block & { - fields: BoardFields -} - -function createBoard(block?: Block): Board { +function createBoard(board?: Board): Board { + const now = Date.now() let cardProperties: IPropertyTemplate[] = [] const selectProperties = cardProperties.find((o) => o.type === 'select') if (!selectProperties) { @@ -47,9 +105,9 @@ function createBoard(block?: Block): Board { cardProperties.push(property) } - if (block?.fields.cardProperties) { + if (board?.cardProperties) { // Deep clone of card properties and their options - cardProperties = block?.fields.cardProperties.map((o: IPropertyTemplate) => { + cardProperties = board?.cardProperties.map((o: IPropertyTemplate) => { return { id: o.id, name: o.name, @@ -60,17 +118,24 @@ function createBoard(block?: Block): Board { } return { - ...createBlock(block), - type: 'board', - fields: { - showDescription: block?.fields.showDescription || false, - description: block?.fields.description || '', - icon: block?.fields.icon || '', - isTemplate: block?.fields.isTemplate || false, - templateVer: block?.fields.templateVer || 0, - columnCalculations: block?.fields.columnCalculations || [], - cardProperties, - }, + id: board?.id || Utils.createGuid(IDType.Board), + teamId: board?.teamId || '', + channelId: board?.channelId || '', + createdBy: board?.createdBy || '', + modifiedBy: board?.modifiedBy || '', + type: board?.type || 'P', + title: board?.title || '', + description: board?.description || '', + icon: board?.icon || '', + showDescription: board?.showDescription || false, + isTemplate: board?.isTemplate || false, + templateVersion: board?.templateVersion || 0, + properties: board?.properties || {}, + cardProperties, + columnCalculations: board?.columnCalculations || {}, + createAt: board?.createAt || now, + updateAt: board?.updateAt || now, + deleteAt: board?.deleteAt || 0, } } @@ -79,4 +144,184 @@ type BoardGroup = { cards: Card[] } -export {Board, PropertyType, IPropertyOption, IPropertyTemplate, BoardGroup, createBoard} +// getPropertiesDifference returns a list of the property IDs that are +// contained in propsA but are not contained in propsB +function getPropertiesDifference(propsA: IPropertyTemplate[], propsB: IPropertyTemplate[]): string[] { + const diff: string[] = [] + propsA.forEach((val) => { + if (!propsB.find((p) => p.id === val.id)) { + diff.push(val.id) + } + }) + + return diff +} + +// isPropertyEqual checks that both the contents of the property and +// its options are equal +function isPropertyEqual(propA: IPropertyTemplate, propB: IPropertyTemplate): boolean { + for (const val of Object.keys(propA)) { + if (val !== 'options' && (propA as any)[val] !== (propB as any)[val]) { + return false + } + } + + if (propA.options.length !== propB.options.length) { + return false + } + + for (const opt of propA.options) { + const optionB = propB.options.find((o) => o.id === opt.id) + if (!optionB) { + return false + } + + for (const val of Object.keys(opt)) { + if ((opt as any)[val] !== (optionB as any)[val]) { + return false + } + } + } + + return true +} + +// createPatchesFromBoards creates two BoardPatch instances, one that +// contains the delta to update the board and another one for the undo +// action, in case it happens +function createPatchesFromBoards(newBoard: Board, oldBoard: Board): BoardPatch[] { + const newDeletedProperties = difference(Object.keys(newBoard.properties || {}), Object.keys(oldBoard.properties || {})) + const newDeletedCardProperties = getPropertiesDifference(newBoard.cardProperties, oldBoard.cardProperties) + const newDeletedColumnCalculations = difference(Object.keys(newBoard.columnCalculations), Object.keys(oldBoard.columnCalculations)) + + const newUpdatedProperties: Record = {} + Object.keys(newBoard.properties || {}).forEach((val) => { + if (oldBoard.properties[val] !== newBoard.properties[val]) { + newUpdatedProperties[val] = newBoard.properties[val] + } + }) + const newUpdatedCardProperties: IPropertyTemplate[] = [] + newBoard.cardProperties.forEach((val) => { + const oldCardProperty = oldBoard.cardProperties.find((o) => o.id === val.id) + if (!oldCardProperty || !isPropertyEqual(val, oldCardProperty)) { + newUpdatedCardProperties.push(val) + } + }) + const newUpdatedColumnCalculations: Record = {} + Object.keys(newBoard.columnCalculations).forEach((val) => { + if (oldBoard.columnCalculations[val] !== newBoard.columnCalculations[val]) { + newUpdatedColumnCalculations[val] = newBoard.columnCalculations[val] + } + }) + + const newData: Record = {} + Object.keys(newBoard).forEach((val) => { + if (val !== 'properties' && + val !== 'cardProperties' && + val !== 'columnCalculations' && + (oldBoard as any)[val] !== (newBoard as any)[val]) { + newData[val] = (newBoard as any)[val] + } + }) + + const oldDeletedProperties = difference(Object.keys(oldBoard.properties || {}), Object.keys(newBoard.properties || {})) + const oldDeletedCardProperties = getPropertiesDifference(oldBoard.cardProperties, newBoard.cardProperties) + const oldDeletedColumnCalculations = difference(Object.keys(oldBoard.columnCalculations), Object.keys(newBoard.columnCalculations)) + + const oldUpdatedProperties: Record = {} + Object.keys(oldBoard.properties || {}).forEach((val) => { + if (newBoard.properties[val] !== oldBoard.properties[val]) { + oldUpdatedProperties[val] = oldBoard.properties[val] + } + }) + const oldUpdatedCardProperties: IPropertyTemplate[] = [] + oldBoard.cardProperties.forEach((val) => { + const newCardProperty = newBoard.cardProperties.find((o) => o.id === val.id) + if (!newCardProperty || !isPropertyEqual(val, newCardProperty)) { + oldUpdatedCardProperties.push(val) + } + }) + const oldUpdatedColumnCalculations: Record = {} + Object.keys(oldBoard.columnCalculations).forEach((val) => { + if (newBoard.columnCalculations[val] !== oldBoard.columnCalculations[val]) { + oldUpdatedColumnCalculations[val] = oldBoard.columnCalculations[val] + } + }) + + const oldData: Record = {} + Object.keys(oldBoard).forEach((val) => { + if (val !== 'properties' && + val !== 'cardProperties' && + val !== 'columnCalculations' && + (newBoard as any)[val] !== (oldBoard as any)[val]) { + oldData[val] = (oldBoard as any)[val] + } + }) + + return [ + { + ...newData, + updatedProperties: newUpdatedProperties, + deletedProperties: oldDeletedProperties, + updatedCardProperties: newUpdatedCardProperties, + deletedCardProperties: oldDeletedCardProperties, + updatedColumnCalculations: newUpdatedColumnCalculations, + deletedColumnCalculations: oldDeletedColumnCalculations, + }, + { + ...oldData, + updatedProperties: oldUpdatedProperties, + deletedProperties: newDeletedProperties, + updatedCardProperties: oldUpdatedCardProperties, + deletedCardProperties: newDeletedCardProperties, + updatedColumnCalculations: oldUpdatedColumnCalculations, + deletedColumnCalculations: newDeletedColumnCalculations, + }, + ] +} + +function createPatchesFromBoardsAndBlocks(updatedBoard: Board, oldBoard: Board, updatedBlockIDs: string[], updatedBlocks: Block[], oldBlocks: Block[]): BoardsAndBlocksPatch[] { + const blockUpdatePatches = [] as BlockPatch[] + const blockUndoPatches = [] as BlockPatch[] + updatedBlocks.forEach((newBlock, i) => { + const [updatePatch, undoPatch] = createPatchesFromBlocks(newBlock, oldBlocks[i]) + blockUpdatePatches.push(updatePatch) + blockUndoPatches.push(undoPatch) + }) + + const [boardUpdatePatch, boardUndoPatch] = createPatchesFromBoards(updatedBoard, oldBoard) + + const updatePatch: BoardsAndBlocksPatch = { + blockIDs: updatedBlockIDs, + blockPatches: blockUpdatePatches, + boardIDs: [updatedBoard.id], + boardPatches: [boardUpdatePatch], + } + + const undoPatch: BoardsAndBlocksPatch = { + blockIDs: updatedBlockIDs, + blockPatches: blockUndoPatches, + boardIDs: [updatedBoard.id], + boardPatches: [boardUndoPatch], + } + + return [updatePatch, undoPatch] +} + +export { + Board, + BoardPatch, + BoardMember, + BoardsAndBlocks, + BoardsAndBlocksPatch, + PropertyType, + IPropertyOption, + IPropertyTemplate, + BoardGroup, + createBoard, + BoardTypes, + BoardTypeOpen, + BoardTypePrivate, + createPatchesFromBoards, + createPatchesFromBoardsAndBlocks, +} diff --git a/webapp/src/blocks/team.ts b/webapp/src/blocks/team.ts new file mode 100644 index 000000000..f17c5adc1 --- /dev/null +++ b/webapp/src/blocks/team.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +interface ITeam { + readonly id: string + readonly title: string + readonly signupToken: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly settings: Readonly> + readonly modifiedBy?: string, + readonly updateAt?: number, +} + +export {ITeam} diff --git a/webapp/src/cardFilter.test.ts b/webapp/src/cardFilter.test.ts index 66a66445b..5046bbe10 100644 --- a/webapp/src/cardFilter.test.ts +++ b/webapp/src/cardFilter.test.ts @@ -15,7 +15,7 @@ const mockedUtils = mocked(Utils, true) describe('src/cardFilter', () => { const board = TestBlockFactory.createBoard() board.id = '1' - board.rootId = '1' + const card1 = TestBlockFactory.createCard(board) card1.id = '1' card1.title = 'card1' diff --git a/webapp/src/components/__snapshots__/blockIconSelector.test.tsx.snap b/webapp/src/components/__snapshots__/blockIconSelector.test.tsx.snap index b91f9469d..a687c6806 100644 --- a/webapp/src/components/__snapshots__/blockIconSelector.test.tsx.snap +++ b/webapp/src/components/__snapshots__/blockIconSelector.test.tsx.snap @@ -3,7 +3,7 @@ exports[`components/blockIconSelector return an icon correctly 1`] = `
`; + +exports[`components/cardDialog should match snapshot without permissions 1`] = ` +
+
+
+ +`; diff --git a/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap b/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap index d0a6f48c3..d1c66a10e 100644 --- a/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap +++ b/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap @@ -53,7 +53,7 @@ exports[`components/centerPanel return centerPanel and click on card to show car class="title" >
-
`; -exports[`components/centerPanel return centerPanel and click on new card to edit template 1`] = ` -
-
-
- -
-
-
- -
-
-
- -
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
- - -
-
- - -
- -
- - -
- -
-
-
- New -
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- - - No name - - -
- - - -
-
-
-
-
- - - - -
- - - -
-
-
-
-
- i -
- -
-
- -
-
-
-
-
-
-
- i -
- -
-
- -
-
-
-
-
-
-
- - - - -
- - - -
-
-
- -
-
-
-`; - exports[`components/centerPanel return centerPanel and press touch 1 with readonly 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- i -
- - board title - + Category 1
+
+ i +
+
+ board title +
+ +
+
+
+
+ +
+ Boards +
+ +
+
+ No boards inside +
+
-
-
-
-
-
-

- Create a Board in Workspace 1 -

-

- Add a board to the sidebar using any of the templates defined below or start from scratch. -
- Members of " - - Workspace 1 - - " will have access to boards created here. -

-
-
-
-
- - - - - New template - -
-
-
-
- - -
-
-
-
-
+ />
`; @@ -1326,62 +1297,42 @@ exports[`src/components/workspace should match snapshot 1`] = ` class="WorkspaceTitle" />
- - Workspace 1 - +
+ + Find Boards + +
-
-
- i -
- - board title - + Category 1
+
+ i +
+
+ board title +
+ +
+
+
+
+ +
+ Boards +
+ +
+
+ No boards inside +
+
-
-
{ name={handler.getDisplayText(intl)} icon={handler.getIcon()} onClick={async () => { - const newBlock = await handler.createBlock(card.rootId) + const newBlock = await handler.createBlock(card.boardId) newBlock.parentId = card.id - newBlock.rootId = card.rootId + newBlock.boardId = card.boardId const typeName = handler.getDisplayText(intl) const description = intl.formatMessage({id: 'ContentBlock.addElement', defaultMessage: 'add {type}'}, {type: typeName}) mutator.performAsUndoGroup(async () => { - const insertedBlock = await mutator.insertBlock(newBlock, description) + const insertedBlock = await mutator.insertBlock(newBlock.boardId, newBlock, description) contentOrder.splice(index, 0, insertedBlock.id) - await mutator.changeCardContentOrder(card.id, card.fields.contentOrder, contentOrder, description) + await mutator.changeCardContentOrder(card.boardId, card.id, card.fields.contentOrder, contentOrder, description) }) }} /> diff --git a/webapp/src/components/blockIconSelector.test.tsx b/webapp/src/components/blockIconSelector.test.tsx index 9a2eb9b26..6df7c93bb 100644 --- a/webapp/src/components/blockIconSelector.test.tsx +++ b/webapp/src/components/blockIconSelector.test.tsx @@ -18,7 +18,7 @@ import {TestBlockFactory} from '../test/testBlockFactory' import BlockIconSelector from './blockIconSelector' -const board = TestBlockFactory.createBoard() +const card = TestBlockFactory.createCard() const icon = '👍' jest.mock('../mutator') @@ -26,23 +26,23 @@ const mockedMutator = mocked(mutator, true) describe('components/blockIconSelector', () => { beforeEach(() => { - board.fields.icon = icon + card.fields.icon = icon jest.clearAllMocks() }) test('return an icon correctly', () => { const {container} = render(wrapIntl( , )) expect(container).toMatchSnapshot() }) test('return no element with no icon', () => { - board.fields.icon = '' + card.fields.icon = '' const {container} = render(wrapIntl( , )) @@ -51,7 +51,7 @@ describe('components/blockIconSelector', () => { test('return menu on click', () => { const {container} = render(wrapIntl( , )) @@ -61,7 +61,7 @@ describe('components/blockIconSelector', () => { test('return no menu in readonly', () => { const {container} = render(wrapIntl( , )) @@ -71,7 +71,7 @@ describe('components/blockIconSelector', () => { test('return a new icon after click on random menu', () => { render(wrapIntl( , )) @@ -79,13 +79,13 @@ describe('components/blockIconSelector', () => { const buttonRandom = screen.queryByRole('button', {name: 'Random'}) expect(buttonRandom).not.toBeNull() userEvent.click(buttonRandom!) - expect(mockedMutator.changeIcon).toBeCalledTimes(1) + expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1) }) test('return a new icon after click on EmojiPicker', async () => { const {container} = render(wrapIntl( , )) @@ -96,14 +96,14 @@ describe('components/blockIconSelector', () => { const allButtonThumbUp = await screen.findAllByRole('button', {name: /thumbsup/i}) userEvent.click(allButtonThumbUp[0]) - expect(mockedMutator.changeIcon).toBeCalledTimes(1) - expect(mockedMutator.changeIcon).toBeCalledWith(board.id, board.fields.icon, '👍') + expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1) + expect(mockedMutator.changeBlockIcon).toBeCalledWith(card.boardId, card.id, card.fields.icon, '👍') }) test('return no icon after click on remove menu', () => { const {container, rerender} = render(wrapIntl( , )) @@ -111,15 +111,15 @@ describe('components/blockIconSelector', () => { const buttonRemove = screen.queryByRole('button', {name: 'Remove icon'}) expect(buttonRemove).not.toBeNull() userEvent.click(buttonRemove!) - expect(mockedMutator.changeIcon).toBeCalledTimes(1) - expect(mockedMutator.changeIcon).toBeCalledWith(board.id, board.fields.icon, '', 'remove icon') + expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1) + expect(mockedMutator.changeBlockIcon).toBeCalledWith(card.boardId, card.id, card.fields.icon, '', 'remove icon') //simulate reset icon - board.fields.icon = '' + card.fields.icon = '' rerender(wrapIntl( ), ) expect(container).toMatchSnapshot() diff --git a/webapp/src/components/blockIconSelector.tsx b/webapp/src/components/blockIconSelector.tsx index f404f9bdd..bf2c5eab3 100644 --- a/webapp/src/components/blockIconSelector.tsx +++ b/webapp/src/components/blockIconSelector.tsx @@ -1,35 +1,28 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React, {useCallback} from 'react' -import {useIntl} from 'react-intl' import {BlockIcons} from '../blockIcons' -import {Board} from '../blocks/board' import {Card} from '../blocks/card' import mutator from '../mutator' -import EmojiPicker from '../widgets/emojiPicker' -import DeleteIcon from '../widgets/icons/delete' -import EmojiIcon from '../widgets/icons/emoji' -import Menu from '../widgets/menu' -import MenuWrapper from '../widgets/menuWrapper' -import './blockIconSelector.scss' + +import IconSelector from './iconSelector' type Props = { - block: Board|Card + block: Card size?: 's' | 'm' | 'l' readonly?: boolean } const BlockIconSelector = (props: Props) => { const {block, size} = props - const intl = useIntl() const onSelectEmoji = useCallback((emoji: string) => { - mutator.changeIcon(block.id, block.fields.icon, emoji) + mutator.changeBlockIcon(block.boardId, block.id, block.fields.icon, emoji) document.body.click() }, [block.id, block.fields.icon]) - const onAddRandomIcon = useCallback(() => mutator.changeIcon(block.id, block.fields.icon, BlockIcons.shared.randomIcon()), [block.id, block.fields.icon]) - const onRemoveIcon = useCallback(() => mutator.changeIcon(block.id, block.fields.icon, '', 'remove icon'), [block.id, block.fields.icon]) + const onAddRandomIcon = useCallback(() => mutator.changeBlockIcon(block.boardId, block.id, block.fields.icon, BlockIcons.shared.randomIcon()), [block.id, block.fields.icon]) + const onRemoveIcon = useCallback(() => mutator.changeBlockIcon(block.boardId, block.id, block.fields.icon, '', 'remove icon'), [block.id, block.fields.icon]) if (!block.fields.icon) { return null @@ -42,35 +35,13 @@ const BlockIconSelector = (props: Props) => { const iconElement =
{block.fields.icon}
return ( -
- {props.readonly && iconElement} - {!props.readonly && - - {iconElement} - - } - name={intl.formatMessage({id: 'ViewTitle.random-icon', defaultMessage: 'Random'})} - onClick={onAddRandomIcon} - /> - } - name={intl.formatMessage({id: 'ViewTitle.pick-icon', defaultMessage: 'Pick icon'})} - > - - - } - name={intl.formatMessage({id: 'ViewTitle.remove-icon', defaultMessage: 'Remove icon'})} - onClick={onRemoveIcon} - /> - - - } -
+ ) } diff --git a/webapp/src/components/boardIconSelector.tsx b/webapp/src/components/boardIconSelector.tsx new file mode 100644 index 000000000..ae2aa239a --- /dev/null +++ b/webapp/src/components/boardIconSelector.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React, {useCallback} from 'react' + +import {BlockIcons} from '../blockIcons' +import {Board} from '../blocks/board' + +import mutator from '../mutator' + +import IconSelector from './iconSelector' + +type Props = { + board: Board + size?: 's' | 'm' | 'l' + readonly?: boolean +} + +const BoardIconSelector = React.memo((props: Props) => { + const {board, size} = props + + const onSelectEmoji = useCallback((emoji: string) => { + mutator.changeBoardIcon(board.id, board.icon, emoji) + document.body.click() + }, [board.id, board.icon]) + const onAddRandomIcon = useCallback(() => mutator.changeBoardIcon(board.id, board.icon, BlockIcons.shared.randomIcon()), [board.id, board.icon]) + const onRemoveIcon = useCallback(() => mutator.changeBoardIcon(board.id, board.icon, '', 'remove board icon'), [board.id, board.icon]) + + if (!board.icon) { + return null + } + + let className = `octo-icon size-${size || 'm'}` + if (props.readonly) { + className += ' readonly' + } + const iconElement =
{board.icon}
+ + return ( + + ) +}) + +export default BoardIconSelector diff --git a/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap b/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap index 276552a2f..bb41d285e 100644 --- a/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap +++ b/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap @@ -20,20 +20,14 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should ma class="title" >
-
.buttons:first-child { + padding-top: 32px; + } } } } diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx index e294b60a1..118f8a6b8 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx @@ -14,7 +14,7 @@ import {MemoryRouter, Router} from 'react-router-dom' import Mutator from '../../mutator' import {Utils} from '../../utils' -import {UserWorkspace} from '../../user' +import {Team} from '../../store/teams' import {mockDOM, mockStateStore, wrapDNDIntl} from '../../testUtils' import BoardTemplateSelector from './boardTemplateSelector' @@ -31,7 +31,7 @@ jest.mock('react-router-dom', () => { }) jest.mock('../../octoClient', () => { return { - getSubtree: jest.fn(() => Promise.resolve([])), + getAllBlocks: jest.fn(() => Promise.resolve([])), } }) jest.mock('../../utils') @@ -40,10 +40,12 @@ jest.mock('../../mutator') describe('components/boardTemplateSelector/boardTemplateSelector', () => { const mockedUtils = mocked(Utils, true) const mockedMutator = mocked(Mutator, true) - const workspace1: UserWorkspace = { - id: 'workspace_1', - title: 'Workspace 1', - boardCount: 1, + const team1: Team = { + id: 'team-1', + title: 'Team 1', + signupToken: '', + updateAt: 0, + modifiedBy: 'user-1', } const template1Title = 'Template 1' const globalTemplateTitle = 'Template Global' @@ -53,42 +55,37 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => { beforeEach(() => { jest.clearAllMocks() const state = { + teams: { + current: team1, + }, users: { me: { id: 'user_id_1', }, }, - workspace: { - userWorkspaces: new Array(workspace1), - current: workspace1, - }, boards: { boards: [ { id: '2', title: boardTitle, - workspaceId: workspace1.id, - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [ - {id: 'id-6'}, - ], - dateDisplayPropertyId: 'id-6', - }, + teamId: team1.id, + icon: '🚴🏻‍♂️', + cardProperties: [ + {id: 'id-6'}, + ], + dateDisplayPropertyId: 'id-6', }, ], templates: [ { id: '1', - workspaceId: workspace1.id, + teamId: team1.id, title: template1Title, - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [ - {id: 'id-5'}, - ], - dateDisplayPropertyId: 'id-5', - }, + icon: '🚴🏻‍♂️', + cardProperties: [ + {id: 'id-5'}, + ], + dateDisplayPropertyId: 'id-5', }, ], cards: [], @@ -98,16 +95,14 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => { value: [{ id: 'global-1', title: globalTemplateTitle, - workspaceId: '0', - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [ - {id: 'global-id-5'}, - ], - dateDisplayPropertyId: 'global-id-5', - isTemplate: true, - templateVer: 2, - }, + teamId: '0', + icon: '🚴🏻‍♂️', + cardProperties: [ + {id: 'global-id-5'}, + ], + dateDisplayPropertyId: 'global-id-5', + isTemplate: true, + templateVersion: 2, }], }, } @@ -221,7 +216,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => { await userEvent.click(deleteConfirm!) }) - expect(mockedMutator.deleteBlock).toBeCalledTimes(1) + expect(mockedMutator.deleteBoard).toBeCalledTimes(1) }) test('return BoardTemplateSelector and click edit template icon', async () => { const history = createMemoryHistory() @@ -259,7 +254,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => { }) await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1)) - await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(expect.anything(), expect.anything(), expect.anything(), expect.anything(), false)) + await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), '1', team1.id)) }) test('return BoardTemplateSelector and click to add board from global template', async () => { render(wrapDNDIntl( @@ -281,7 +276,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => { userEvent.click(useTemplateButton!) }) await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1)) - await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(expect.anything(), expect.anything(), expect.anything(), expect.anything(), true)) + await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), 'global-1', team1.id)) }) }) }) diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx index a986f0dd8..ec7186c9e 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx @@ -11,7 +11,8 @@ import AddIcon from '../../widgets/icons/add' import Button from '../../widgets/buttons/button' import octoClient from '../../octoClient' import mutator from '../../mutator' -import {getTemplates, getCurrentBoard} from '../../store/boards' +import {getTemplates, getCurrentBoardId} from '../../store/boards' +import {getCurrentTeam, Team} from '../../store/teams' import {fetchGlobalTemplates, getGlobalTemplates} from '../../store/globalTemplates' import {useAppDispatch, useAppSelector} from '../../store/hooks' import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' @@ -33,7 +34,8 @@ type Props = { const BoardTemplateSelector = (props: Props) => { const globalTemplates = useAppSelector(getGlobalTemplates) || [] - const currentBoard = useAppSelector(getCurrentBoard) || null + const currentBoardId = useAppSelector(getCurrentBoardId) || null + const currentTeam = useAppSelector(getCurrentTeam) const {title, description, onClose} = props const dispatch = useAppDispatch() const intl = useIntl() @@ -52,18 +54,17 @@ const BoardTemplateSelector = (props: Props) => { }, [match, history, onClose]) useEffect(() => { - if (octoClient.workspaceId !== '0' && globalTemplates.length === 0) { + if (octoClient.teamId !== '0' && globalTemplates.length === 0) { dispatch(fetchGlobalTemplates()) } - }, [octoClient.workspaceId]) + }, [octoClient.teamId]) const onBoardTemplateDelete = useCallback((template: Board) => { TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteBoardTemplate, {board: template.id}) - mutator.deleteBlock( + mutator.deleteBoard( template, intl.formatMessage({id: 'BoardTemplateSelector.delete-template', defaultMessage: 'Delete template'}), - async () => { - }, + async () => {}, async () => { showBoard(template.id) }, @@ -95,7 +96,7 @@ const BoardTemplateSelector = (props: Props) => { } const handleUseTemplate = async () => { - await mutator.addBoardFromTemplate(intl, showBoard, () => showBoard(currentBoard.id), activeTemplate.id, activeTemplate.workspaceId === '0') + await mutator.addBoardFromTemplate(currentTeam?.id || '0', intl, showBoard, () => showBoard(currentBoardId), activeTemplate.id, currentTeam?.id) if (activeTemplate.title === OnboardingBoardTitle) { resetTour() } @@ -157,7 +158,7 @@ const BoardTemplateSelector = (props: Props) => { ))}
mutator.addEmptyBoardTemplate(intl, showBoard, () => showBoard(currentBoard.id))} + onClick={() => mutator.addEmptyBoardTemplate(currentTeam?.id || '', intl, showBoard, () => showBoard(currentBoardId))} > @@ -186,7 +187,7 @@ const BoardTemplateSelector = (props: Props) => { filled={false} emphasis={'secondary'} size={'medium'} - onClick={() => mutator.addEmptyBoard(intl, showBoard, () => showBoard(currentBoard.id))} + onClick={() => mutator.addEmptyBoard(currentTeam?.id || '', intl, showBoard, () => showBoard(currentBoardId))} > { getSubtree: jest.fn(() => Promise.resolve([ { id: '1', - workspaceId: 'workspace', + teamId: 'team', title: 'Template', - type: 'board', - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [groupProperty], - dateDisplayPropertyId: 'id-5', - }, + icon: '🚴🏻‍♂️', + cardProperties: [groupProperty], + dateDisplayPropertyId: 'id-5', }, { id: '2', - workspaceId: 'workspace', + boardId: '1', title: 'View', type: 'view', fields: { @@ -69,7 +66,7 @@ jest.mock('../../octoClient', () => { }, { id: '3', - workspaceId: 'workspace', + boardId: '1', title: 'Card', type: 'card', fields: { @@ -88,48 +85,42 @@ jest.mock('../../mutator') describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => { const template: Board = { id: '1', - workspaceId: 'workspace_1', + teamId: 'team-1', title: 'Template 1', createdBy: 'user-1', modifiedBy: 'user-1', createAt: 10, updateAt: 20, deleteAt: 0, + description: 'test', + showDescription: false, type: 'board', - parentId: '123', - rootId: '123', - schema: 1, - fields: { - description: 'test', - icon: '🚴🏻‍♂️', - cardProperties: [groupProperty], - dateDisplayPropertyId: 'id-5', - columnCalculations: {}, - }, + isTemplate: true, + templateVersion: 0, + icon: '🚴🏻‍♂️', + cardProperties: [groupProperty], + columnCalculations: {}, + properties: {}, } const globalTemplate: Board = { id: 'global-1', title: 'Template global', - workspaceId: '0', + teamId: '0', createdBy: 'user-1', modifiedBy: 'user-1', createAt: 10, updateAt: 20, deleteAt: 0, type: 'board', - parentId: '123', - rootId: '123', - schema: 1, - fields: { - icon: '🚴🏻‍♂️', - description: 'test', - cardProperties: [groupProperty], - dateDisplayPropertyId: 'global-id-5', - columnCalculations: {}, - isTemplate: true, - templateVer: 2, - }, + icon: '🚴🏻‍♂️', + description: 'test', + showDescription: false, + cardProperties: [groupProperty], + columnCalculations: {}, + isTemplate: true, + templateVersion: 2, + properties: {}, } beforeEach(() => { diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx index 0d0899b04..9c7308857 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx @@ -36,9 +36,9 @@ const BoardTemplateSelectorItem = (props: Props) => { className={isActive ? 'BoardTemplateSelectorItem active' : 'BoardTemplateSelectorItem'} onClick={onClickHandler} > - {template.fields.icon} + {template.icon} {template.title} - {!template.fields.templateVer && + {!template.templateVersion &&
} diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.test.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.test.tsx index 74ff1f911..15ecb4e46 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.test.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.test.tsx @@ -6,7 +6,6 @@ import {MockStoreEnhanced} from 'redux-mock-store' import {Provider as ReduxProvider} from 'react-redux' -import {UserWorkspace} from '../../user' import {IPropertyTemplate} from '../../blocks/board' import {mockDOM, mockStateStore, wrapDNDIntl} from '../../testUtils' @@ -43,17 +42,15 @@ const groupProperty: IPropertyTemplate = { jest.mock('../../octoClient', () => { return { - getSubtree: jest.fn(() => Promise.resolve([ + getAllBlocks: jest.fn(() => Promise.resolve([ { id: '1', - workspaceId: 'workspace', + teamId: 'team', title: 'Template', type: 'board', - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [groupProperty], - dateDisplayPropertyId: 'id-5', - }, + icon: '🚴🏻‍♂️', + cardProperties: [groupProperty], + dateDisplayPropertyId: 'id-5', }, { id: '2', @@ -89,11 +86,6 @@ jest.mock('../../utils') jest.mock('../../mutator') describe('components/boardTemplateSelector/boardTemplateSelectorPreview', () => { - const workspace1: UserWorkspace = { - id: 'workspace_1', - title: 'Workspace 1', - boardCount: 1, - } const template1Title = 'Template 1' const globalTemplateTitle = 'Template Global' const boardTitle = 'Board 1' @@ -104,12 +96,10 @@ describe('components/boardTemplateSelector/boardTemplateSelectorPreview', () => const board = { id: '2', title: boardTitle, - workspaceId: workspace1.id, - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [groupProperty], - dateDisplayPropertyId: 'id-6', - }, + teamId: 'team-id', + icon: '🚴🏻‍♂️', + cardProperties: [groupProperty], + dateDisplayPropertyId: 'id-6', } const state = { @@ -132,39 +122,40 @@ describe('components/boardTemplateSelector/boardTemplateSelectorPreview', () => views: {views: []}, contents: {contents: []}, comments: {comments: []}, - workspace: { - userWorkspaces: new Array(workspace1), - current: workspace1, + teams: { + current: {id: 'team-id'}, }, boards: { - boards: [board], + current: board.id, + boards: { + [board.id]: board, + }, templates: [ { id: '1', - workspaceId: workspace1.id, + teamId: 'team-id', title: template1Title, - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [groupProperty], - dateDisplayPropertyId: 'id-5', - }, + icon: '🚴🏻‍♂️', + cardProperties: [groupProperty], + dateDisplayPropertyId: 'id-5', }, ], cards: [], views: [], + myBoardMemberships: { + [board.id]: {userId: 'user-id', schemeAdmin: true}, + }, }, globalTemplates: { value: [{ id: 'global-1', title: globalTemplateTitle, - workspaceId: '0', - fields: { - icon: '🚴🏻‍♂️', - cardProperties: [ - {id: 'global-id-5'}, - ], - dateDisplayPropertyId: 'global-id-5', - }, + teamId: '0', + icon: '🚴🏻‍♂️', + cardProperties: [ + {id: 'global-id-5'}, + ], + dateDisplayPropertyId: 'global-id-5', }], }, } diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx index 02d8314cb..30b6a6a03 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx @@ -31,7 +31,7 @@ const BoardTemplateSelectorPreview = (props: Props) => { setActiveTemplateCards([]) setActiveView(null) setActiveTemplateCards([]) - octoClient.getSubtree(activeTemplate.id, activeView?.fields.viewType === 'gallery' ? 3 : 2, activeTemplate.workspaceId).then((blocks) => { + octoClient.getAllBlocks(activeTemplate.id).then((blocks) => { const cards = blocks.filter((b) => b.type === 'card') const views = blocks.filter((b) => b.type === 'view').sort((a, b) => a.title.localeCompare(b.title)) if (views.length > 0) { @@ -45,11 +45,11 @@ const BoardTemplateSelectorPreview = (props: Props) => { }, [activeTemplate]) const dateDisplayProperty = useMemo(() => { - return activeTemplate?.fields.cardProperties.find((o) => o.id === activeView?.fields.dateDisplayPropertyId) + return activeTemplate?.cardProperties.find((o) => o.id === activeView?.fields.dateDisplayPropertyId) }, [activeView, activeTemplate]) const groupByProperty = useMemo(() => { - return activeTemplate?.fields.cardProperties.find((o) => o.id === activeView?.fields.groupById) || activeTemplate?.fields.cardProperties[0] + return activeTemplate?.cardProperties.find((o) => o.id === activeView?.fields.groupById) || activeTemplate?.cardProperties[0] }, [activeView, activeTemplate]) const {visible: visibleGroups, hidden: hiddenGroups} = useMemo(() => { diff --git a/webapp/src/components/boardsSwitcher/boardsSwitcher.scss b/webapp/src/components/boardsSwitcher/boardsSwitcher.scss new file mode 100644 index 000000000..335039c2c --- /dev/null +++ b/webapp/src/components/boardsSwitcher/boardsSwitcher.scss @@ -0,0 +1,28 @@ +.BoardsSwitcherWrapper { + display: flex; + flex-direction: row; + padding: 0 16px; + + .BoardsSwitcher { + display: flex; + flex-direction: row; + background-color: rgba(var(--sidebar-text-rgb), 0.08); + flex: 1; + padding: 6px; + gap: 6px; + border-radius: 4px; + cursor: pointer; + } + + .CompassIcon.icon-magnify.CompassIcon { + font-size: 16px; + } + + .add-board-icon { + margin-left: 8px; + padding-top: 4px; + cursor: pointer; + font-size: 16px; + width: 16px; + } +} diff --git a/webapp/src/components/boardsSwitcher/boardsSwitcher.tsx b/webapp/src/components/boardsSwitcher/boardsSwitcher.tsx new file mode 100644 index 000000000..ba2353660 --- /dev/null +++ b/webapp/src/components/boardsSwitcher/boardsSwitcher.tsx @@ -0,0 +1,82 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React, {useEffect, useState} from 'react' + +import {useIntl} from 'react-intl' + +import Search from '../../widgets/icons/search' + +import './boardsSwitcher.scss' +import AddIcon from '../../widgets/icons/add' +import BoardSwitcherDialog from '../boardsSwitcherDialog/boardSwitcherDialog' +import {Utils} from '../../utils' +import {Constants} from '../../constants' + +type Props = { + onBoardTemplateSelectorOpen?: () => void, +} + +const BoardsSwitcher = (props: Props): JSX.Element => { + const intl = useIntl() + + const [showSwitcher, setShowSwitcher] = useState(false) + + // We need this keyboard handling (copied from Mattermost webapp) instead of + // using react-hotkeys-hook as react-hotkeys-hook is unable to handle keyboard shortcuts that + // the browser uses when the user is focused in an input field. + // + // For example, you press Cmd + k, then type something in the search input field. Pressing Cmd + k again + // is expected to close the board switcher, however, with react-hotkeys-hook it doesn't. + // This is because Cmd + k is a Firefox shortcut and react-hotkeys-hook is + // unable to override it if the user is focused on any input field. + const handleQuickSwitchKeyPress = (e: KeyboardEvent) => { + if (Utils.cmdOrCtrlPressed(e) && !e.shiftKey && Utils.isKeyPressed(e, Constants.keyCodes.K)) { + if (!e.altKey) { + e.preventDefault() + setShowSwitcher((show) => !show) + } + } + } + + useEffect(() => { + document.addEventListener('keydown', handleQuickSwitchKeyPress) + + // cleanup function + return () => { + document.removeEventListener('keydown', handleQuickSwitchKeyPress) + } + }, []) + + return ( +
+
setShowSwitcher(true)} + > + +
+ + {intl.formatMessage({id: 'BoardsSwitcher.Title', defaultMessage: 'Find Boards'})} + +
+
+ + { + Utils.isFocalboardPlugin() && + + + + } + + { + showSwitcher && + setShowSwitcher(false)}/> + } +
+ ) +} + +export default BoardsSwitcher diff --git a/webapp/src/components/boardsSwitcherDialog/boardSwitcherDialog.scss b/webapp/src/components/boardsSwitcherDialog/boardSwitcherDialog.scss new file mode 100644 index 000000000..5e564e54a --- /dev/null +++ b/webapp/src/components/boardsSwitcherDialog/boardSwitcherDialog.scss @@ -0,0 +1,19 @@ +.blockSearchResult { + display: flex; + gap: 12px; + overflow: hidden; + flex-direction: row; + + .CompassIcon { + font-size: 18px; + color: #484848; + } + + span { + display: inline-block; + height: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/webapp/src/components/boardsSwitcherDialog/boardSwitcherDialog.tsx b/webapp/src/components/boardsSwitcherDialog/boardSwitcherDialog.tsx new file mode 100644 index 000000000..51498cccc --- /dev/null +++ b/webapp/src/components/boardsSwitcherDialog/boardSwitcherDialog.tsx @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React, {ReactNode} from 'react' + +import './boardSwitcherDialog.scss' +import {useIntl} from 'react-intl' + +import {generatePath, useHistory, useRouteMatch} from 'react-router-dom' + +import octoClient from '../../octoClient' +import SearchDialog from '../searchDialog/searchDialog' +import Globe from '../../widgets/icons/globe' +import LockOutline from '../../widgets/icons/lockOutline' +import {useAppSelector} from '../../store/hooks' +import {getCurrentTeam} from '../../store/teams' +import {getMe} from '../../store/users' +import {BoardTypeOpen, BoardTypePrivate} from '../../blocks/board' + +type Props = { + onClose: () => void +} + +const BoardSwitcherDialog = (props: Props): JSX.Element => { + const intl = useIntl() + const team = useAppSelector(getCurrentTeam) + const me = useAppSelector(getMe) + const title = intl.formatMessage({id: 'FindBoardsDialog.Title', defaultMessage: 'Find Boards'}) + const subTitle = intl.formatMessage( + { + id: 'FindBoardsDialog.SubTitle', + defaultMessage: 'Type to find a board. Use UP/DOWN to browse. ENTER to select, ESC to dismiss', + }, + { + b: (...chunks) => {chunks}, + }, + ) + + const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string}>() + const history = useHistory() + + const selectBoard = async (boardId: string): Promise => { + if (!me) { + return + } + const newPath = generatePath(match.path, {...match.params, boardId, viewId: undefined}) + history.push(newPath) + props.onClose() + } + + const searchHandler = async (query: string): Promise> => { + if (query.trim().length === 0 || !team) { + return [] + } + + const items = await octoClient.search(team.id, query) + const untitledBoardTitle = intl.formatMessage({id: 'ViewTitle.untitled-board', defaultMessage: 'Untitled Board'}) + return items.map((item) => ( +
selectBoard(item.id)} + > + {item.type === BoardTypeOpen && } + {item.type === BoardTypePrivate && } + {item.title || untitledBoardTitle} +
+ )) + } + + return ( + + ) +} + +export default BoardSwitcherDialog diff --git a/webapp/src/components/calendar/__snapshots__/fullCalendar.test.tsx.snap b/webapp/src/components/calendar/__snapshots__/fullCalendar.test.tsx.snap index 2223c2aa3..825fba18b 100644 --- a/webapp/src/components/calendar/__snapshots__/fullCalendar.test.tsx.snap +++ b/webapp/src/components/calendar/__snapshots__/fullCalendar.test.tsx.snap @@ -278,7 +278,7 @@ exports[`components/calendar/toolbar return calendar, no date property 1`] = ` id="fc-dom-2" >
+
+ + +
+
+ 6 +
+
+ +
+
+
+
+
+
+ + + + +
+
+
+ + + + +
+
+
+
+
+`; + +exports[`components/calendar/toolbar return calendar, without permissions 1`] = ` +
+
+
+
+
+

+ October 2021 +

+
+
+
+ + +
+ + + +
+
+
+
+
+ + + + + + +