mirror of
https://github.com/grafana/grafana.git
synced 2024-12-02 13:39:19 -06:00
bc2813ef06
* fix goimports * fix goimports order
311 lines
9.0 KiB
Go
311 lines
9.0 KiB
Go
package folderimpl
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/VividCortex/mysqlerr"
|
|
"github.com/go-sql-driver/mysql"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/slugify"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
type sqlStore struct {
|
|
db db.DB
|
|
log log.Logger
|
|
cfg *setting.Cfg
|
|
fm featuremgmt.FeatureToggles
|
|
}
|
|
|
|
// sqlStore implements the store interface.
|
|
var _ store = (*sqlStore)(nil)
|
|
|
|
func ProvideStore(db db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles) *sqlStore {
|
|
return &sqlStore{db: db, log: log.New("folder-store"), cfg: cfg, fm: features}
|
|
}
|
|
|
|
func (ss *sqlStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) {
|
|
if cmd.UID == "" {
|
|
return nil, folder.ErrBadRequest.Errorf("missing UID")
|
|
}
|
|
|
|
var foldr *folder.Folder
|
|
/*
|
|
version := 1
|
|
updatedBy := cmd.SignedInUser.UserID
|
|
createdBy := cmd.SignedInUser.UserID
|
|
*/
|
|
var lastInsertedID int64
|
|
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
|
var sql string
|
|
var args []interface{}
|
|
if cmd.ParentUID == "" {
|
|
sql = "INSERT INTO folder(org_id, uid, title, description, created, updated) VALUES(?, ?, ?, ?, ?, ?)"
|
|
args = []interface{}{cmd.OrgID, cmd.UID, cmd.Title, cmd.Description, time.Now(), time.Now()}
|
|
} else {
|
|
if cmd.ParentUID != folder.GeneralFolderUID {
|
|
if _, err := ss.Get(ctx, folder.GetFolderQuery{
|
|
UID: &cmd.ParentUID,
|
|
OrgID: cmd.OrgID,
|
|
}); err != nil {
|
|
return folder.ErrFolderNotFound.Errorf("parent folder does not exist")
|
|
}
|
|
}
|
|
sql = "INSERT INTO folder(org_id, uid, parent_uid, title, description, created, updated) VALUES(?, ?, ?, ?, ?, ?, ?)"
|
|
args = []interface{}{cmd.OrgID, cmd.UID, cmd.ParentUID, cmd.Title, cmd.Description, time.Now(), time.Now()}
|
|
}
|
|
|
|
var err error
|
|
lastInsertedID, err = sess.WithReturningID(ss.db.GetDialect().DriverName(), sql, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
foldr, err = ss.Get(ctx, folder.GetFolderQuery{
|
|
ID: &lastInsertedID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
return foldr, err
|
|
}
|
|
|
|
func (ss *sqlStore) Delete(ctx context.Context, uid string, orgID int64) error {
|
|
return ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
|
_, err := sess.Exec("DELETE FROM folder WHERE uid=? AND org_id=?", uid, orgID)
|
|
if err != nil {
|
|
return folder.ErrDatabaseError.Errorf("failed to delete folder: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) {
|
|
updated := time.Now()
|
|
uid := cmd.UID
|
|
|
|
var foldr *folder.Folder
|
|
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
|
sql := strings.Builder{}
|
|
sql.Write([]byte("UPDATE folder SET "))
|
|
columnsToUpdate := []string{"updated = ?"}
|
|
args := []interface{}{updated}
|
|
if cmd.NewDescription != nil {
|
|
columnsToUpdate = append(columnsToUpdate, "description = ?")
|
|
args = append(args, *cmd.NewDescription)
|
|
}
|
|
|
|
if cmd.NewTitle != nil {
|
|
columnsToUpdate = append(columnsToUpdate, "title = ?")
|
|
args = append(args, *cmd.NewTitle)
|
|
}
|
|
|
|
if cmd.NewUID != nil {
|
|
columnsToUpdate = append(columnsToUpdate, "uid = ?")
|
|
uid = *cmd.NewUID
|
|
args = append(args, *cmd.NewUID)
|
|
}
|
|
|
|
if cmd.NewParentUID != nil {
|
|
columnsToUpdate = append(columnsToUpdate, "parent_uid = ?")
|
|
args = append(args, *cmd.NewParentUID)
|
|
}
|
|
|
|
if len(columnsToUpdate) == 0 {
|
|
return folder.ErrBadRequest.Errorf("no columns to update")
|
|
}
|
|
|
|
sql.Write([]byte(strings.Join(columnsToUpdate, ", ")))
|
|
sql.Write([]byte(" WHERE uid = ? AND org_id = ?"))
|
|
args = append(args, cmd.UID, cmd.OrgID)
|
|
|
|
args = append([]interface{}{sql.String()}, args...)
|
|
|
|
res, err := sess.Exec(args...)
|
|
if err != nil {
|
|
return folder.ErrDatabaseError.Errorf("failed to update folder: %w", err)
|
|
}
|
|
|
|
affected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return folder.ErrInternal.Errorf("failed to get affected row: %w", err)
|
|
}
|
|
if affected == 0 {
|
|
return folder.ErrInternal.Errorf("no folders are updated")
|
|
}
|
|
|
|
foldr, err = ss.Get(ctx, folder.GetFolderQuery{
|
|
UID: &uid,
|
|
OrgID: cmd.OrgID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return foldr, err
|
|
}
|
|
|
|
func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) {
|
|
foldr := &folder.Folder{}
|
|
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
|
exists := false
|
|
var err error
|
|
switch {
|
|
case q.UID != nil:
|
|
exists, err = sess.SQL("SELECT * FROM folder WHERE uid = ? AND org_id = ?", q.UID, q.OrgID).Get(foldr)
|
|
case q.ID != nil:
|
|
exists, err = sess.SQL("SELECT * FROM folder WHERE id = ?", q.ID).Get(foldr)
|
|
case q.Title != nil:
|
|
exists, err = sess.SQL("SELECT * FROM folder WHERE title = ? AND org_id = ?", q.Title, q.OrgID).Get(foldr)
|
|
default:
|
|
return folder.ErrBadRequest.Errorf("one of ID, UID, or Title must be included in the command")
|
|
}
|
|
if err != nil {
|
|
return folder.ErrDatabaseError.Errorf("failed to get folder: %w", err)
|
|
}
|
|
if !exists {
|
|
return folder.ErrFolderNotFound.Errorf("folder not found")
|
|
}
|
|
return nil
|
|
})
|
|
foldr.URL = dashboards.GetFolderURL(foldr.UID, slugify.Slugify(foldr.Title))
|
|
return foldr, err
|
|
}
|
|
|
|
func (ss *sqlStore) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
|
var folders []*folder.Folder
|
|
|
|
recQuery := `
|
|
WITH RECURSIVE RecQry AS (
|
|
SELECT * FROM folder WHERE uid = ? AND org_id = ?
|
|
UNION ALL SELECT f.* FROM folder f INNER JOIN RecQry r ON f.uid = r.parent_uid and f.org_id = r.org_id
|
|
)
|
|
SELECT * FROM RecQry;
|
|
`
|
|
|
|
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
|
err := sess.SQL(recQuery, q.UID, q.OrgID).Find(&folders)
|
|
if err != nil {
|
|
return folder.ErrDatabaseError.Errorf("failed to get folder parents: %w", err)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
var driverErr *mysql.MySQLError
|
|
if errors.As(err, &driverErr) {
|
|
if driverErr.Number == mysqlerr.ER_PARSE_ERROR {
|
|
ss.log.Debug("recursive CTE subquery is not supported; it fallbacks to the iterative implementation")
|
|
return ss.getParentsMySQL(ctx, q)
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if len(folders) < 1 {
|
|
// the query is expected to return at least the same folder
|
|
// if it's empty it means that the folder does not exist
|
|
return nil, folder.ErrFolderNotFound
|
|
}
|
|
|
|
return util.Reverse(folders[1:]), nil
|
|
}
|
|
|
|
func (ss *sqlStore) GetChildren(ctx context.Context, q folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
|
var folders []*folder.Folder
|
|
|
|
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
|
sql := strings.Builder{}
|
|
args := make([]interface{}, 0, 2)
|
|
if q.UID == "" {
|
|
sql.Write([]byte("SELECT * FROM folder WHERE parent_uid IS NULL AND org_id=?"))
|
|
args = append(args, q.OrgID)
|
|
} else {
|
|
sql.Write([]byte("SELECT * FROM folder WHERE parent_uid=? AND org_id=?"))
|
|
args = append(args, q.UID, q.OrgID)
|
|
}
|
|
|
|
if q.Limit != 0 {
|
|
var offset int64 = 0
|
|
if q.Page > 0 {
|
|
offset = q.Limit * (q.Page - 1)
|
|
}
|
|
sql.Write([]byte(ss.db.GetDialect().LimitOffset(q.Limit, offset)))
|
|
}
|
|
err := sess.SQL(sql.String(), args...).Find(&folders)
|
|
if err != nil {
|
|
return folder.ErrDatabaseError.Errorf("failed to get folder children: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
return folders, err
|
|
}
|
|
|
|
func (ss *sqlStore) getParentsMySQL(ctx context.Context, cmd folder.GetParentsQuery) (folders []*folder.Folder, err error) {
|
|
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
|
uid := ""
|
|
ok, err := sess.SQL("SELECT parent_uid FROM folder WHERE org_id=? AND uid=?", cmd.OrgID, cmd.UID).Get(&uid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return folder.ErrFolderNotFound
|
|
}
|
|
for {
|
|
f := &folder.Folder{}
|
|
ok, err := sess.SQL("SELECT * FROM folder WHERE org_id=? AND uid=?", cmd.OrgID, uid).Get(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
break
|
|
}
|
|
folders = append(folders, f)
|
|
uid = f.ParentUID
|
|
if len(folders) > folder.MaxNestedFolderDepth {
|
|
return folder.ErrFolderTooDeep
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return util.Reverse(folders), err
|
|
}
|
|
|
|
func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) {
|
|
height := -1
|
|
queue := []string{foldrUID}
|
|
for len(queue) > 0 && height <= folder.MaxNestedFolderDepth {
|
|
length := len(queue)
|
|
height++
|
|
for i := 0; i < length; i++ {
|
|
ele := queue[0]
|
|
queue = queue[1:]
|
|
if parentUID != nil && *parentUID == ele {
|
|
return 0, folder.ErrCircularReference
|
|
}
|
|
folders, err := ss.GetChildren(ctx, folder.GetChildrenQuery{UID: ele, OrgID: orgID})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for _, f := range folders {
|
|
queue = append(queue, f.UID)
|
|
}
|
|
}
|
|
}
|
|
if height > folder.MaxNestedFolderDepth {
|
|
ss.log.Warn("folder height exceeds the maximum allowed depth, You might have a circular reference", "uid", foldrUID, "orgId", orgID, "maxDepth", folder.MaxNestedFolderDepth)
|
|
}
|
|
return height, nil
|
|
}
|