瀏覽代碼

folders: changes and updated tests after merging permissions and new url structure

Marcus Efraimsson 8 年之前
父節點
當前提交
04a9a650e7
共有 5 個文件被更改,包括 242 次插入42 次删除
  1. 4 3
      pkg/api/api.go
  2. 2 1
      pkg/api/dtos/folder.go
  3. 19 21
      pkg/api/folder.go
  4. 167 11
      pkg/api/folder_test.go
  5. 50 6
      pkg/models/folders.go

+ 4 - 3
pkg/api/api.go

@@ -248,10 +248,11 @@ func (hs *HttpServer) registerRoutes() {
 
 		// Folders
 		apiRoute.Group("/folders", func(folderRoute RouteRegister) {
-			folderRoute.Get("/:id", wrap(GetFolderById))
+			folderRoute.Get("/:uid", wrap(GetFolder))
+			folderRoute.Get("/id/:id", wrap(GetFolder))
 			folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
-			folderRoute.Put("/:id", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
-			folderRoute.Delete("/:id", wrap(DeleteFolder))
+			folderRoute.Put("/:uid", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
+			folderRoute.Delete("/:uid", wrap(DeleteFolder))
 		})
 
 		// Dashboard

+ 2 - 1
pkg/api/dtos/folder.go

@@ -4,8 +4,9 @@ import "time"
 
 type Folder struct {
 	Id        int64     `json:"id"`
+	Uid       string    `json:"uid"`
 	Title     string    `json:"title"`
-	Slug      string    `json:"slug"`
+	Url       string    `json:"url"`
 	HasAcl    bool      `json:"hasAcl"`
 	CanSave   bool      `json:"canSave"`
 	CanEdit   bool      `json:"canEdit"`

+ 19 - 21
pkg/api/folder.go

@@ -12,8 +12,8 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 )
 
-func getFolderHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) {
-	query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
+func getFolderHelper(orgId int64, id int64, uid string) (*m.Dashboard, Response) {
+	query := m.GetDashboardQuery{OrgId: orgId, Id: id, Uid: uid}
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrDashboardNotFound {
 			err = m.ErrFolderNotFound
@@ -53,8 +53,8 @@ func GetFoldersForSignedInUser(c *middleware.Context) Response {
 	return Json(200, query.Result)
 }
 
-func GetFolderById(c *middleware.Context) Response {
-	folder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id"))
+func GetFolder(c *middleware.Context) Response {
+	folder, rsp := getFolderHelper(c.OrgId, c.ParamsInt64(":id"), c.Params(":uid"))
 	if rsp != nil {
 		return rsp
 	}
@@ -72,14 +72,13 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.UserId = c.UserId
 
-	dashFolder := m.NewDashboardFolder(cmd.Title)
+	dashFolder := cmd.GetDashboardModel()
 
 	guardian := guardian.NewDashboardGuardian(0, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return folderGuardianResponse(err)
 	}
 
-	// Check if Title is empty
 	if dashFolder.Title == "" {
 		return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil)
 	}
@@ -92,9 +91,6 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response {
 		return ApiError(403, "Quota reached", nil)
 	}
 
-	dashFolder.CreatedBy = c.UserId
-	dashFolder.UpdatedBy = c.UserId
-
 	dashItem := &dashboards.SaveDashboardItem{
 		Dashboard: dashFolder,
 		OrgId:     c.OrgId,
@@ -113,8 +109,9 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response {
 func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.UserId = c.UserId
+	uid := c.Params(":uid")
 
-	dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id"))
+	dashFolder, rsp := getFolderHelper(c.OrgId, 0, uid)
 	if rsp != nil {
 		return rsp
 	}
@@ -124,13 +121,8 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response {
 		return folderGuardianResponse(err)
 	}
 
-	dashFolder.Data.Set("title", cmd.Title)
-	dashFolder.Title = cmd.Title
-	dashFolder.Data.Set("version", cmd.Version)
-	dashFolder.Version = cmd.Version
-	dashFolder.UpdatedBy = c.UserId
+	cmd.UpdateDashboardModel(dashFolder)
 
-	// Check if Title is empty
 	if dashFolder.Title == "" {
 		return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil)
 	}
@@ -139,6 +131,7 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response {
 		Dashboard: dashFolder,
 		OrgId:     c.OrgId,
 		UserId:    c.UserId,
+		Overwrite: cmd.Overwrite,
 	}
 
 	folder, err := dashboards.GetRepository().SaveDashboard(dashItem)
@@ -151,7 +144,7 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response {
 }
 
 func DeleteFolder(c *middleware.Context) Response {
-	dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id"))
+	dashFolder, rsp := getFolderHelper(c.OrgId, 0, c.Params(":uid"))
 	if rsp != nil {
 		return rsp
 	}
@@ -177,17 +170,18 @@ func toDto(guardian *guardian.DashboardGuardian, folder *m.Dashboard) dtos.Folde
 
 	// Finding creator and last updater of the folder
 	updater, creator := "Anonymous", "Anonymous"
-	if folder.UpdatedBy > 0 {
-		updater = getUserLogin(folder.UpdatedBy)
-	}
 	if folder.CreatedBy > 0 {
 		creator = getUserLogin(folder.CreatedBy)
 	}
+	if folder.UpdatedBy > 0 {
+		updater = getUserLogin(folder.UpdatedBy)
+	}
 
 	return dtos.Folder{
 		Id:        folder.Id,
+		Uid:       folder.Uid,
 		Title:     folder.Title,
-		Slug:      folder.Slug,
+		Url:       folder.GetUrl(),
 		HasAcl:    folder.HasAcl,
 		CanSave:   canSave,
 		CanEdit:   canEdit,
@@ -205,6 +199,10 @@ func toFolderError(err error) Response {
 		return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil)
 	}
 
+	if err == m.ErrDashboardWithSameNameInFolderExists {
+		return Json(412, util.DynMap{"status": "name-exists", "message": m.ErrFolderSameNameExists.Error()})
+	}
+
 	if err == m.ErrDashboardWithSameUIDExists {
 		return Json(412, util.DynMap{"status": "uid-exists", "message": m.ErrFolderWithSameUIDExists.Error()})
 	}

+ 167 - 11
pkg/api/folder_test.go

@@ -23,8 +23,11 @@ func TestFoldersApiEndpoint(t *testing.T) {
 		fakeDash.FolderId = 1
 		fakeDash.HasAcl = false
 
+		var getDashboardQueries []*m.GetDashboardQuery
+
 		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
 			query.Result = fakeDash
+			getDashboardQueries = append(getDashboardQueries, query)
 			return nil
 		})
 
@@ -33,19 +36,40 @@ func TestFoldersApiEndpoint(t *testing.T) {
 		Convey("When user is an Org Editor", func() {
 			role := m.ROLE_EDITOR
 
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
+				callGetFolder(sc)
+				So(sc.resp.Code, ShouldEqual, 404)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
 				callGetFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 404)
+
+				Convey("Should lookup folder by id", func() {
+					So(getDashboardQueries[0].Id, ShouldEqual, 1)
+				})
 			})
 
-			updateFolderScenario("When calling PUT on", "/api/folders/1", "/api/folders/:id", role, updateFolderCmd, func(sc *scenarioContext) {
+			updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) {
 				callUpdateFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 404)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
 			})
 
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
 				callDeleteFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 404)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
 			})
 		})
 	})
@@ -55,8 +79,11 @@ func TestFoldersApiEndpoint(t *testing.T) {
 		fakeFolder.Id = 1
 		fakeFolder.HasAcl = false
 
+		var getDashboardQueries []*m.GetDashboardQuery
+
 		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
 			query.Result = fakeFolder
+			getDashboardQueries = append(getDashboardQueries, query)
 			return nil
 		})
 
@@ -82,12 +109,34 @@ func TestFoldersApiEndpoint(t *testing.T) {
 			Title: fakeFolder.Title,
 		}
 
+		updateFolderCmd := m.UpdateFolderCommand{
+			Title: fakeFolder.Title,
+		}
+
 		Convey("When user is an Org Viewer", func() {
 			role := m.ROLE_VIEWER
 
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
+				folder := getFolderShouldReturn200(sc)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+
+				Convey("Should not be able to edit or save folder", func() {
+					So(folder.CanEdit, ShouldBeFalse)
+					So(folder.CanSave, ShouldBeFalse)
+					So(folder.CanAdmin, ShouldBeFalse)
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
 				folder := getFolderShouldReturn200(sc)
 
+				Convey("Should lookup folder by id", func() {
+					So(getDashboardQueries[0].Id, ShouldEqual, 1)
+				})
+
 				Convey("Should not be able to edit or save folder", func() {
 					So(folder.CanEdit, ShouldBeFalse)
 					So(folder.CanSave, ShouldBeFalse)
@@ -95,23 +144,54 @@ func TestFoldersApiEndpoint(t *testing.T) {
 				})
 			})
 
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
 				callDeleteFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
 			})
 
 			createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
 				callCreateFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
+
+			updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) {
+				callUpdateFolder(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+			})
 		})
 
 		Convey("When user is an Org Editor", func() {
 			role := m.ROLE_EDITOR
 
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
+				folder := getFolderShouldReturn200(sc)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+
+				Convey("Should be able to edit or save folder", func() {
+					So(folder.CanEdit, ShouldBeTrue)
+					So(folder.CanSave, ShouldBeTrue)
+					So(folder.CanAdmin, ShouldBeFalse)
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
 				folder := getFolderShouldReturn200(sc)
 
+				Convey("Should lookup folder by id", func() {
+					So(getDashboardQueries[0].Id, ShouldEqual, 1)
+				})
+
 				Convey("Should be able to edit or save folder", func() {
 					So(folder.CanEdit, ShouldBeTrue)
 					So(folder.CanSave, ShouldBeTrue)
@@ -119,15 +199,28 @@ func TestFoldersApiEndpoint(t *testing.T) {
 				})
 			})
 
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
 				callDeleteFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
 			})
 
 			createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
 				callCreateFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
+
+			updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) {
+				callUpdateFolder(sc)
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+			})
 		})
 	})
 
@@ -136,8 +229,11 @@ func TestFoldersApiEndpoint(t *testing.T) {
 		fakeFolder.Id = 1
 		fakeFolder.HasAcl = true
 
+		var getDashboardQueries []*m.GetDashboardQuery
+
 		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
 			query.Result = fakeFolder
+			getDashboardQueries = append(getDashboardQueries, query)
 			return nil
 		})
 
@@ -163,50 +259,110 @@ func TestFoldersApiEndpoint(t *testing.T) {
 			Title: fakeFolder.Title,
 		}
 
+		updateFolderCmd := m.UpdateFolderCommand{
+			Title: fakeFolder.Title,
+		}
+
 		Convey("When user is an Org Viewer and has no permissions for this folder", func() {
 			role := m.ROLE_VIEWER
 
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
+				callGetFolder(sc)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+
+				Convey("Should be denied access", func() {
+					So(sc.resp.Code, ShouldEqual, 403)
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
-				sc.handlerFunc = GetFolderById
-				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+				callGetFolder(sc)
+
+				Convey("Should lookup folder by id", func() {
+					So(getDashboardQueries[0].Id, ShouldEqual, 1)
+				})
 
 				Convey("Should be denied access", func() {
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 			})
 
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
 				callDeleteFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
 			})
 
 			createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
 				callCreateFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
+
+			updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) {
+				callUpdateFolder(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+			})
 		})
 
 		Convey("When user is an Org Editor and has no permissions for this folder", func() {
 			role := m.ROLE_EDITOR
 
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
+				callGetFolder(sc)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+
+				Convey("Should be denied access", func() {
+					So(sc.resp.Code, ShouldEqual, 403)
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
-				sc.handlerFunc = GetFolderById
-				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+				callGetFolder(sc)
+
+				Convey("Should lookup folder by id", func() {
+					So(getDashboardQueries[0].Id, ShouldEqual, 1)
+				})
 
 				Convey("Should be denied access", func() {
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 			})
 
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) {
 				callDeleteFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
 			})
 
 			createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
 				callCreateFolder(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
+
+			updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) {
+				callUpdateFolder(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup folder by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "uid")
+				})
+			})
 		})
 	})
 }
@@ -224,7 +380,7 @@ func getFolderShouldReturn200(sc *scenarioContext) dtos.Folder {
 }
 
 func callGetFolder(sc *scenarioContext) {
-	sc.handlerFunc = GetFolderById
+	sc.handlerFunc = GetFolder
 	sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
 }
 

+ 50 - 6
pkg/models/folders.go

@@ -2,6 +2,7 @@ package models
 
 import (
 	"errors"
+	"strings"
 	"time"
 )
 
@@ -10,14 +11,16 @@ var (
 	ErrFolderNotFound                = errors.New("Folder not found")
 	ErrFolderVersionMismatch         = errors.New("The folder has been changed by someone else")
 	ErrFolderTitleEmpty              = errors.New("Folder title cannot be empty")
-	ErrFolderWithSameUIDExists       = errors.New("A folder with the same uid already exists")
+	ErrFolderWithSameUIDExists       = errors.New("A folder/dashboard with the same uid already exists")
+	ErrFolderSameNameExists          = errors.New("A folder or dashboard in the general folder with the same name already exists")
 	ErrFolderFailedGenerateUniqueUid = errors.New("Failed to generate unique folder id")
 )
 
 type Folder struct {
 	Id      int64
+	Uid     string
 	Title   string
-	Slug    string
+	Url     string
 	OrgId   int64
 	Version int
 
@@ -29,6 +32,45 @@ type Folder struct {
 	HasAcl    bool
 }
 
+// GetDashboardModel turns the command into the savable model
+func (cmd *CreateFolderCommand) GetDashboardModel() *Dashboard {
+	dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title))
+	dashFolder.OrgId = cmd.OrgId
+	dashFolder.Uid = strings.TrimSpace(cmd.Uid)
+	dashFolder.Data.Set("uid", cmd.Uid)
+
+	userId := cmd.UserId
+
+	if userId == 0 {
+		userId = -1
+	}
+
+	dashFolder.CreatedBy = userId
+	dashFolder.UpdatedBy = userId
+	dashFolder.UpdateSlug()
+
+	return dashFolder
+}
+
+// UpdateDashboardModel updates an existing model from command into model for update
+func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard) {
+	dashFolder.Title = strings.TrimSpace(cmd.Title)
+	dashFolder.Data.Set("title", cmd.Title)
+	dashFolder.Uid = dashFolder.Data.MustString("uid")
+	dashFolder.Data.Set("version", cmd.Version)
+	dashFolder.Version = cmd.Version
+	dashFolder.IsFolder = true
+
+	userId := cmd.UserId
+
+	if userId == 0 {
+		userId = -1
+	}
+
+	dashFolder.UpdatedBy = userId
+	dashFolder.UpdateSlug()
+}
+
 //
 // COMMANDS
 //
@@ -36,16 +78,18 @@ type Folder struct {
 type CreateFolderCommand struct {
 	OrgId  int64  `json:"-"`
 	UserId int64  `json:"userId"`
+	Uid    string `json:"uid"`
 	Title  string `json:"title"`
 
 	Result *Folder
 }
 
 type UpdateFolderCommand struct {
-	OrgId   int64  `json:"-"`
-	UserId  int64  `json:"userId"`
-	Title   string `json:"title"`
-	Version int    `json:"version"`
+	OrgId     int64  `json:"-"`
+	UserId    int64  `json:"userId"`
+	Title     string `json:"title"`
+	Version   int    `json:"version"`
+	Overwrite bool   `json:"overwrite"`
 
 	Result *Folder
 }