From fb379ae43672e4775b26f997023ec232bfb17fe0 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:26:35 +0300 Subject: [PATCH] Chore: Introduce playlist service (#52252) * Store: Introduce playlist service * Integrate playlist service * Update swagger --- pkg/api/docs/definitions/playlists.go | 16 +- pkg/api/http_server.go | 5 +- pkg/api/playlist.go | 67 +++--- pkg/server/wire.go | 2 + pkg/services/playlist/model.go | 95 ++++++++ pkg/services/playlist/playlist.go | 14 ++ .../playlist/playlistimpl/playlist.go | 44 ++++ pkg/services/playlist/playlistimpl/store.go | 226 ++++++++++++++++++ .../playlist/playlistimpl/store_test.go | 82 +++++++ pkg/services/playlist/playlisttest/fake.go | 43 ++++ pkg/services/sqlstore/store.go | 6 + public/api-merged.json | 37 ++- public/api-spec.json | 8 +- 13 files changed, 587 insertions(+), 58 deletions(-) create mode 100644 pkg/services/playlist/model.go create mode 100644 pkg/services/playlist/playlist.go create mode 100644 pkg/services/playlist/playlistimpl/playlist.go create mode 100644 pkg/services/playlist/playlistimpl/store.go create mode 100644 pkg/services/playlist/playlistimpl/store_test.go create mode 100644 pkg/services/playlist/playlisttest/fake.go diff --git a/pkg/api/docs/definitions/playlists.go b/pkg/api/docs/definitions/playlists.go index 321a137601b..03989c4d153 100644 --- a/pkg/api/docs/definitions/playlists.go +++ b/pkg/api/docs/definitions/playlists.go @@ -2,7 +2,7 @@ package definitions import ( "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/playlist" ) // swagger:route GET /playlists playlists searchPlaylists @@ -121,7 +121,7 @@ type DeletePlaylistParams struct { type UpdatePlaylistParams struct { // in:body // required:true - Body models.UpdatePlaylistCommand + Body playlist.UpdatePlaylistCommand // in:path // required:true UID string `json:"uid"` @@ -131,28 +131,28 @@ type UpdatePlaylistParams struct { type CreatePlaylistParams struct { // in:body // required:true - Body models.CreatePlaylistCommand + Body playlist.CreatePlaylistCommand } // swagger:response searchPlaylistsResponse type SearchPlaylistsResponse struct { // The response message // in: body - Body models.Playlists `json:"body"` + Body playlist.Playlists `json:"body"` } // swagger:response getPlaylistResponse type GetPlaylistResponse struct { // The response message // in: body - Body *models.PlaylistDTO `json:"body"` + Body *playlist.PlaylistDTO `json:"body"` } // swagger:response getPlaylistItemsResponse type GetPlaylistItemsResponse struct { // The response message // in: body - Body []models.PlaylistItemDTO `json:"body"` + Body []playlist.PlaylistItemDTO `json:"body"` } // swagger:response getPlaylistDashboardsResponse @@ -166,12 +166,12 @@ type GetPlaylistDashboardsResponse struct { type UpdatePlaylistResponseResponse struct { // The response message // in: body - Body *models.PlaylistDTO `json:"body"` + Body *playlist.PlaylistDTO `json:"body"` } // swagger:response createPlaylistResponse type CreatePlaylistResponse struct { // The response message // in: body - Body *models.Playlist `json:"body"` + Body *playlist.Playlist `json:"body"` } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 8152dbf0004..532a0b201f1 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -58,6 +58,7 @@ import ( "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/plugindashboards" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" pref "github.com/grafana/grafana/pkg/services/preference" @@ -168,6 +169,7 @@ type HTTPServer struct { dashboardVersionService dashver.Service PublicDashboardsApi *publicdashboardsApi.Api starService star.Service + playlistService playlist.Service CoremodelRegistry *registry.Generic CoremodelStaticRegistry *registry.Static kvStore kvstore.KVStore @@ -206,7 +208,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, entityEventsService store.EntityEventsService, teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, - starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static, + starService star.Service, playlistService playlist.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, publicDashboardsApi *publicdashboardsApi.Api, userService user.Service) (*HTTPServer, error) { web.Env = cfg.Env @@ -289,6 +291,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dashboardPermissionsService: dashboardPermissionsService, dashboardVersionService: dashboardVersionService, starService: starService, + playlistService: playlistService, CoremodelRegistry: coremodelRegistry, CoremodelStaticRegistry: coremodelStaticRegistry, kvStore: kvStore, diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 47889948e13..6c85aec19d8 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -6,25 +6,26 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/web" ) func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) { uid := web.Params(c.Req)[":uid"] - query := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} - err := hs.SQLStore.GetPlaylist(c.Req.Context(), &query) + query := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} + p, err := hs.playlistService.Get(c.Req.Context(), &query) if err != nil { c.JsonApiErr(404, "Playlist not found", err) return } - if query.Result.OrgId == 0 { + if p.OrgId == 0 { c.JsonApiErr(404, "Playlist not found", err) return } - if query.Result.OrgId != c.OrgId { + if p.OrgId != c.OrgId { c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil) return } @@ -38,53 +39,54 @@ func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response { limit = 1000 } - searchQuery := models.GetPlaylistsQuery{ + searchQuery := playlist.GetPlaylistsQuery{ Name: query, Limit: limit, OrgId: c.OrgId, } - err := hs.SQLStore.SearchPlaylists(c.Req.Context(), &searchQuery) + playlists, err := hs.playlistService.Search(c.Req.Context(), &searchQuery) if err != nil { return response.Error(500, "Search failed", err) } - return response.JSON(http.StatusOK, searchQuery.Result) + return response.JSON(http.StatusOK, playlists) } func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] - cmd := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} + cmd := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} - if err := hs.SQLStore.GetPlaylist(c.Req.Context(), &cmd); err != nil { + p, err := hs.playlistService.Get(c.Req.Context(), &cmd) + if err != nil { return response.Error(500, "Playlist not found", err) } playlistDTOs, _ := hs.LoadPlaylistItemDTOs(c.Req.Context(), uid, c.OrgId) - dto := &models.PlaylistDTO{ - Id: cmd.Result.Id, - UID: cmd.Result.UID, - Name: cmd.Result.Name, - Interval: cmd.Result.Interval, - OrgId: cmd.Result.OrgId, + dto := &playlist.PlaylistDTO{ + Id: p.Id, + UID: p.UID, + Name: p.Name, + Interval: p.Interval, + OrgId: p.OrgId, Items: playlistDTOs, } return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItemDTO, error) { +func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgId int64) ([]playlist.PlaylistItemDTO, error) { playlistitems, err := hs.LoadPlaylistItems(ctx, uid, orgId) if err != nil { return nil, err } - playlistDTOs := make([]models.PlaylistItemDTO, 0) + playlistDTOs := make([]playlist.PlaylistItemDTO, 0) for _, item := range playlistitems { - playlistDTOs = append(playlistDTOs, models.PlaylistItemDTO{ + playlistDTOs = append(playlistDTOs, playlist.PlaylistItemDTO{ Id: item.Id, PlaylistId: item.PlaylistId, Type: item.Type, @@ -97,13 +99,14 @@ func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgI return playlistDTOs, nil } -func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItem, error) { - itemQuery := models.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: orgId} - if err := hs.SQLStore.GetPlaylistItem(ctx, &itemQuery); err != nil { +func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, uid string, orgId int64) ([]playlist.PlaylistItem, error) { + itemQuery := playlist.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: orgId} + items, err := hs.playlistService.GetItems(ctx, &itemQuery) + if err != nil { return nil, err } - return *itemQuery.Result, nil + return items, nil } func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response { @@ -132,8 +135,8 @@ func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Respo func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] - cmd := models.DeletePlaylistCommand{UID: uid, OrgId: c.OrgId} - if err := hs.SQLStore.DeletePlaylist(c.Req.Context(), &cmd); err != nil { + cmd := playlist.DeletePlaylistCommand{UID: uid, OrgId: c.OrgId} + if err := hs.playlistService.Delete(c.Req.Context(), &cmd); err != nil { return response.Error(500, "Failed to delete playlist", err) } @@ -141,28 +144,30 @@ func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { } func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { - cmd := models.CreatePlaylistCommand{} + cmd := playlist.CreatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } cmd.OrgId = c.OrgId - if err := hs.SQLStore.CreatePlaylist(c.Req.Context(), &cmd); err != nil { + p, err := hs.playlistService.Create(c.Req.Context(), &cmd) + if err != nil { return response.Error(500, "Failed to create playlist", err) } - return response.JSON(http.StatusOK, cmd.Result) + return response.JSON(http.StatusOK, p) } func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { - cmd := models.UpdatePlaylistCommand{} + cmd := playlist.UpdatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } cmd.OrgId = c.OrgId cmd.UID = web.Params(c.Req)[":uid"] - if err := hs.SQLStore.UpdatePlaylist(c.Req.Context(), &cmd); err != nil { + p, err := hs.playlistService.Update(c.Req.Context(), &cmd) + if err != nil { return response.Error(500, "Failed to save playlist", err) } @@ -171,6 +176,6 @@ func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { return response.Error(500, "Failed to save playlist", err) } - cmd.Result.Items = playlistDTOs - return response.JSON(http.StatusOK, cmd.Result) + p.Items = playlistDTOs + return response.JSON(http.StatusOK, p) } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 7af7d59a278..70ef423e848 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -6,6 +6,7 @@ package server import ( "github.com/google/wire" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/services/playlist/playlistimpl" "github.com/grafana/grafana/pkg/services/store/sanitizer" "github.com/grafana/grafana/pkg/api" @@ -286,6 +287,7 @@ var wireBasicSet = wire.NewSet( ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), starimpl.ProvideService, + playlistimpl.ProvideService, dashverimpl.ProvideService, publicdashboardsService.ProvideService, wire.Bind(new(publicdashboards.Service), new(*publicdashboardsService.PublicDashboardServiceImpl)), diff --git a/pkg/services/playlist/model.go b/pkg/services/playlist/model.go new file mode 100644 index 00000000000..b310edba74c --- /dev/null +++ b/pkg/services/playlist/model.go @@ -0,0 +1,95 @@ +package playlist + +import ( + "errors" +) + +// Typed errors +var ( + ErrPlaylistNotFound = errors.New("Playlist not found") + ErrPlaylistFailedGenerateUniqueUid = errors.New("failed to generate unique playlist UID") + ErrCommandValidationFailed = errors.New("command missing required fields") +) + +// Playlist model +type Playlist struct { + Id int64 `json:"id"` + UID string `json:"uid" xorm:"uid"` + Name string `json:"name"` + Interval string `json:"interval"` + OrgId int64 `json:"-"` +} + +type PlaylistDTO struct { + Id int64 `json:"id"` + UID string `json:"uid"` + Name string `json:"name"` + Interval string `json:"interval"` + OrgId int64 `json:"-"` + Items []PlaylistItemDTO `json:"items"` +} + +type PlaylistItemDTO struct { + Id int64 `json:"id"` + PlaylistId int64 `json:"playlistid"` + Type string `json:"type"` + Title string `json:"title"` + Value string `json:"value"` + Order int `json:"order"` +} + +type PlaylistItem struct { + Id int64 + PlaylistId int64 + Type string + Value string + Order int + Title string +} + +type Playlists []*Playlist + +// +// COMMANDS +// + +type UpdatePlaylistCommand struct { + OrgId int64 `json:"-"` + UID string `json:"uid"` + Name string `json:"name" binding:"Required"` + Interval string `json:"interval"` + Items []PlaylistItemDTO `json:"items"` +} + +type CreatePlaylistCommand struct { + Name string `json:"name" binding:"Required"` + Interval string `json:"interval"` + Items []PlaylistItemDTO `json:"items"` + + OrgId int64 `json:"-"` +} + +type DeletePlaylistCommand struct { + UID string + OrgId int64 +} + +// +// QUERIES +// + +type GetPlaylistsQuery struct { + Name string + Limit int + OrgId int64 +} + +type GetPlaylistByUidQuery struct { + UID string + OrgId int64 +} + +type GetPlaylistItemsByUidQuery struct { + PlaylistUID string + OrgId int64 +} diff --git a/pkg/services/playlist/playlist.go b/pkg/services/playlist/playlist.go new file mode 100644 index 00000000000..fbea7a84817 --- /dev/null +++ b/pkg/services/playlist/playlist.go @@ -0,0 +1,14 @@ +package playlist + +import ( + "context" +) + +type Service interface { + Create(context.Context, *CreatePlaylistCommand) (*Playlist, error) + Update(context.Context, *UpdatePlaylistCommand) (*PlaylistDTO, error) + Get(context.Context, *GetPlaylistByUidQuery) (*Playlist, error) + GetItems(context.Context, *GetPlaylistItemsByUidQuery) ([]PlaylistItem, error) + Search(context.Context, *GetPlaylistsQuery) (Playlists, error) + Delete(ctx context.Context, cmd *DeletePlaylistCommand) error +} diff --git a/pkg/services/playlist/playlistimpl/playlist.go b/pkg/services/playlist/playlistimpl/playlist.go new file mode 100644 index 00000000000..de2a8588051 --- /dev/null +++ b/pkg/services/playlist/playlistimpl/playlist.go @@ -0,0 +1,44 @@ +package playlistimpl + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/services/sqlstore/db" +) + +type Service struct { + store store +} + +func ProvideService(db db.DB) playlist.Service { + return &Service{ + store: &sqlStore{ + db: db, + }, + } +} + +func (s *Service) Create(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) { + return s.store.Insert(ctx, cmd) +} + +func (s *Service) Update(ctx context.Context, cmd *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) { + return s.store.Update(ctx, cmd) +} + +func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) { + return s.store.Get(ctx, q) +} + +func (s *Service) GetItems(ctx context.Context, q *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) { + return s.store.GetItems(ctx, q) +} + +func (s *Service) Search(ctx context.Context, q *playlist.GetPlaylistsQuery) (playlist.Playlists, error) { + return s.store.List(ctx, q) +} + +func (s *Service) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error { + return s.store.Delete(ctx, cmd) +} diff --git a/pkg/services/playlist/playlistimpl/store.go b/pkg/services/playlist/playlistimpl/store.go new file mode 100644 index 00000000000..121e3144ffc --- /dev/null +++ b/pkg/services/playlist/playlistimpl/store.go @@ -0,0 +1,226 @@ +package playlistimpl + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/db" + "github.com/grafana/grafana/pkg/util" +) + +type store interface { + Insert(context.Context, *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) + Delete(context.Context, *playlist.DeletePlaylistCommand) error + Get(context.Context, *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) + GetItems(context.Context, *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) + List(context.Context, *playlist.GetPlaylistsQuery) (playlist.Playlists, error) + Update(context.Context, *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) +} + +type sqlStore struct { + db db.DB +} + +func (s *sqlStore) Insert(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) { + p := playlist.Playlist{} + err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + uid, err := generateAndValidateNewPlaylistUid(sess, cmd.OrgId) + if err != nil { + return err + } + + p = playlist.Playlist{ + Name: cmd.Name, + Interval: cmd.Interval, + OrgId: cmd.OrgId, + UID: uid, + } + + _, err = sess.Insert(&p) + if err != nil { + return err + } + + playlistItems := make([]playlist.PlaylistItem, 0) + for _, item := range cmd.Items { + playlistItems = append(playlistItems, playlist.PlaylistItem{ + PlaylistId: p.Id, + Type: item.Type, + Value: item.Value, + Order: item.Order, + Title: item.Title, + }) + } + + _, err = sess.Insert(&playlistItems) + + return err + }) + return &p, err +} + +func (s *sqlStore) Update(ctx context.Context, cmd *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) { + dto := playlist.PlaylistDTO{} + err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + p := playlist.Playlist{ + UID: cmd.UID, + OrgId: cmd.OrgId, + Name: cmd.Name, + Interval: cmd.Interval, + } + + existingPlaylist := playlist.Playlist{UID: cmd.UID, OrgId: cmd.OrgId} + _, err := sess.Get(&existingPlaylist) + if err != nil { + return err + } + p.Id = existingPlaylist.Id + + dto = playlist.PlaylistDTO{ + + Id: p.Id, + UID: p.UID, + OrgId: p.OrgId, + Name: p.Name, + Interval: p.Interval, + } + + _, err = sess.Where("id=?", p.Id).Cols("name", "interval").Update(&p) + if err != nil { + return err + } + + rawSQL := "DELETE FROM playlist_item WHERE playlist_id = ?" + _, err = sess.Exec(rawSQL, p.Id) + + if err != nil { + return err + } + + playlistItems := make([]models.PlaylistItem, 0) + + for index, item := range cmd.Items { + playlistItems = append(playlistItems, models.PlaylistItem{ + PlaylistId: p.Id, + Type: item.Type, + Value: item.Value, + Order: index + 1, + Title: item.Title, + }) + } + + _, err = sess.Insert(&playlistItems) + return err + }) + return &dto, err +} + +func (s *sqlStore) Get(ctx context.Context, query *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) { + if query.UID == "" || query.OrgId == 0 { + return nil, playlist.ErrCommandValidationFailed + } + + p := playlist.Playlist{} + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + p = playlist.Playlist{UID: query.UID, OrgId: query.OrgId} + exists, err := sess.Get(&p) + if !exists { + return playlist.ErrPlaylistNotFound + } + + return err + }) + return &p, err +} + +func (s *sqlStore) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error { + if cmd.UID == "" || cmd.OrgId == 0 { + return playlist.ErrCommandValidationFailed + } + + return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + playlist := playlist.Playlist{UID: cmd.UID, OrgId: cmd.OrgId} + _, err := sess.Get(&playlist) + if err != nil { + return err + } + + var rawPlaylistSQL = "DELETE FROM playlist WHERE uid = ? and org_id = ?" + _, err = sess.Exec(rawPlaylistSQL, cmd.UID, cmd.OrgId) + if err != nil { + return err + } + + var rawItemSQL = "DELETE FROM playlist_item WHERE playlist_id = ?" + _, err = sess.Exec(rawItemSQL, playlist.Id) + + return err + }) +} + +func (s *sqlStore) List(ctx context.Context, query *playlist.GetPlaylistsQuery) (playlist.Playlists, error) { + playlists := make(playlist.Playlists, 0) + if query.OrgId == 0 { + return playlists, playlist.ErrCommandValidationFailed + } + + err := s.db.WithDbSession(ctx, func(dbSess *sqlstore.DBSession) error { + sess := dbSess.Limit(query.Limit) + + if query.Name != "" { + sess.Where("name LIKE ?", "%"+query.Name+"%") + } + + sess.Where("org_id = ?", query.OrgId) + err := sess.Find(&playlists) + + return err + }) + return playlists, err +} + +func (s *sqlStore) GetItems(ctx context.Context, query *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) { + var playlistItems = make([]playlist.PlaylistItem, 0) + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + if query.PlaylistUID == "" || query.OrgId == 0 { + return models.ErrCommandValidationFailed + } + + // getQuery the playlist Id + getQuery := &playlist.GetPlaylistByUidQuery{UID: query.PlaylistUID, OrgId: query.OrgId} + p, err := s.Get(ctx, getQuery) + if err != nil { + return err + } + + err = sess.Where("playlist_id=?", p.Id).Find(&playlistItems) + + return err + }) + return playlistItems, err +} + +// generateAndValidateNewPlaylistUid generates a playlistUID and verifies that +// the uid isn't already in use. This is deliberately overly cautious, since users +// can also specify playlist uids during provisioning. +func generateAndValidateNewPlaylistUid(sess *sqlstore.DBSession, orgId int64) (string, error) { + for i := 0; i < 3; i++ { + uid := generateNewUid() + + playlist := models.Playlist{OrgId: orgId, UID: uid} + exists, err := sess.Get(&playlist) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", models.ErrPlaylistFailedGenerateUniqueUid +} + +var generateNewUid func() string = util.GenerateShortUID diff --git a/pkg/services/playlist/playlistimpl/store_test.go b/pkg/services/playlist/playlistimpl/store_test.go new file mode 100644 index 00000000000..0eb1fe842aa --- /dev/null +++ b/pkg/services/playlist/playlistimpl/store_test.go @@ -0,0 +1,82 @@ +package playlistimpl + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestIntegrationPlaylistDataAccess(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ss := sqlstore.InitTestDB(t) + playlistStore := sqlStore{db: ss} + + t.Run("Can create playlist", func(t *testing.T) { + items := []playlist.PlaylistItemDTO{ + {Title: "graphite", Value: "graphite", Type: "dashboard_by_tag"}, + {Title: "Backend response times", Value: "3", Type: "dashboard_by_id"}, + } + cmd := playlist.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items} + p, err := playlistStore.Insert(context.Background(), &cmd) + require.NoError(t, err) + uid := p.UID + + t.Run("Can get playlist items", func(t *testing.T) { + get := &playlist.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: 1} + storedPlaylistItems, err := playlistStore.GetItems(context.Background(), get) + require.NoError(t, err) + require.Equal(t, len(storedPlaylistItems), len(items)) + }) + + t.Run("Can update playlist", func(t *testing.T) { + items := []playlist.PlaylistItemDTO{ + {Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"}, + {Title: "Backend response times", Value: "2", Type: "dashboard_by_id"}, + } + query := playlist.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, UID: uid, Interval: "10s", Items: items} + _, err = playlistStore.Update(context.Background(), &query) + require.NoError(t, err) + }) + + t.Run("Can remove playlist", func(t *testing.T) { + deleteQuery := playlist.DeletePlaylistCommand{UID: uid, OrgId: 1} + err = playlistStore.Delete(context.Background(), &deleteQuery) + require.NoError(t, err) + + getQuery := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: 1} + p, err := playlistStore.Get(context.Background(), &getQuery) + require.Error(t, err) + require.Equal(t, uid, p.UID, "playlist should've been removed") + require.ErrorIs(t, err, playlist.ErrPlaylistNotFound) + }) + }) + + t.Run("Delete playlist that doesn't exist", func(t *testing.T) { + deleteQuery := playlist.DeletePlaylistCommand{UID: "654312", OrgId: 1} + err := playlistStore.Delete(context.Background(), &deleteQuery) + require.NoError(t, err) + }) + + t.Run("Delete playlist with invalid command yields error", func(t *testing.T) { + testCases := []struct { + desc string + cmd playlist.DeletePlaylistCommand + }{ + {desc: "none", cmd: playlist.DeletePlaylistCommand{}}, + {desc: "no OrgId", cmd: playlist.DeletePlaylistCommand{UID: "1"}}, + {desc: "no Uid", cmd: playlist.DeletePlaylistCommand{OrgId: 1}}, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + err := playlistStore.Delete(context.Background(), &tc.cmd) + require.EqualError(t, err, playlist.ErrCommandValidationFailed.Error()) + }) + } + }) +} diff --git a/pkg/services/playlist/playlisttest/fake.go b/pkg/services/playlist/playlisttest/fake.go new file mode 100644 index 00000000000..2f99a88c5b1 --- /dev/null +++ b/pkg/services/playlist/playlisttest/fake.go @@ -0,0 +1,43 @@ +package playlisttest + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/playlist" +) + +type FakePlaylistService struct { + ExpectedPlaylist *playlist.Playlist + ExpectedPlaylistDTO *playlist.PlaylistDTO + ExpectedPlaylistItems []playlist.PlaylistItem + ExpectedPlaylists playlist.Playlists + ExpectedError error +} + +func NewPlaylistServiveFake() *FakePlaylistService { + return &FakePlaylistService{} +} + +func (f *FakePlaylistService) Create(context.Context, *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) { + return f.ExpectedPlaylist, f.ExpectedError +} + +func (f *FakePlaylistService) Update(context.Context, *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) { + return f.ExpectedPlaylistDTO, f.ExpectedError +} + +func (f *FakePlaylistService) Get(context.Context, *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) { + return f.ExpectedPlaylist, f.ExpectedError +} + +func (f *FakePlaylistService) GetItems(context.Context, *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) { + return f.ExpectedPlaylistItems, f.ExpectedError +} + +func (f *FakePlaylistService) Search(context.Context, *playlist.GetPlaylistsQuery) (playlist.Playlists, error) { + return f.ExpectedPlaylists, f.ExpectedError +} + +func (f *FakePlaylistService) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error { + return f.ExpectedError +} diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index 55bc7f85a3d..bcba1b75781 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -72,11 +72,17 @@ type Store interface { GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error WithTransactionalDbSession(ctx context.Context, callback DBTransactionFunc) error InTransaction(ctx context.Context, fn func(ctx context.Context) error) error + // deprecated CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error + // deprecated UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error + // deprecated GetPlaylist(ctx context.Context, query *models.GetPlaylistByUidQuery) error + // deprecated DeletePlaylist(ctx context.Context, cmd *models.DeletePlaylistCommand) error + // deprecated SearchPlaylists(ctx context.Context, query *models.GetPlaylistsQuery) error + // deprecated GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByUidQuery) error GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error diff --git a/public/api-merged.json b/public/api-merged.json index c83c8d61fb2..98ab31b0a08 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -8535,6 +8535,14 @@ "tags": ["provisioning"], "summary": "Get all the contact points.", "operationId": "RouteGetContactpoints", + "parameters": [ + { + "type": "string", + "description": "Filter by name", + "name": "name", + "in": "query" + } + ], "responses": { "200": { "description": "ContactPoints", @@ -8874,6 +8882,20 @@ } } } + }, + "delete": { + "consumes": ["application/json"], + "tags": ["provisioning"], + "summary": "Clears the notification policy tree.", + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + } } }, "/v1/provisioning/templates": { @@ -10250,7 +10272,7 @@ "$ref": "#/definitions/ScheduleDTO" }, "state": { - "type": "string" + "$ref": "#/definitions/State" }, "templateVars": { "type": "object" @@ -10268,9 +10290,6 @@ "CreatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/Playlist" - }, "interval": { "type": "string" }, @@ -15496,9 +15515,8 @@ "type": "string" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -15768,9 +15786,6 @@ "UpdatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/PlaylistDTO" - }, "interval": { "type": "string" }, @@ -16298,6 +16313,7 @@ } }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": ["alerts", "labels", "receiver"], "properties": { @@ -16461,6 +16477,7 @@ } }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt", "id", "status", "updatedAt"], "properties": { @@ -16500,7 +16517,6 @@ } }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -16634,7 +16650,6 @@ } }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": ["name"], "properties": { diff --git a/public/api-spec.json b/public/api-spec.json index b438b6d5e39..e9275cccdff 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -9380,7 +9380,7 @@ "$ref": "#/definitions/ScheduleDTO" }, "state": { - "type": "string" + "$ref": "#/definitions/State" }, "templateVars": { "type": "object" @@ -9398,9 +9398,6 @@ "CreatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/Playlist" - }, "interval": { "type": "string" }, @@ -12676,9 +12673,6 @@ "UpdatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/PlaylistDTO" - }, "interval": { "type": "string" },