diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 4aa4ccdfc4e..0fbb0d19949 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -89,6 +89,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/db" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/services/star/starimpl" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/teamguardian" teamguardianDatabase "github.com/grafana/grafana/pkg/services/teamguardian/database" @@ -258,6 +259,7 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), + starimpl.ProvideService, ) var wireSet = wire.NewSet( diff --git a/pkg/services/star/model.go b/pkg/services/star/model.go new file mode 100644 index 00000000000..cf8e3aa882b --- /dev/null +++ b/pkg/services/star/model.go @@ -0,0 +1,54 @@ +package star + +import "errors" + +var ErrCommandValidationFailed = errors.New("command missing required fields") + +type Star struct { + ID int64 `xorm:"pk autoincr 'id'"` + UserID int64 `xorm:"user_id"` + DashboardID int64 `xorm:"dashboard_id"` +} + +// ---------------------- +// COMMANDS + +type StarDashboardCommand struct { + UserID int64 `xorm:"user_id"` + DashboardID int64 `xorm:"dashboard_id"` +} + +func (cmd *StarDashboardCommand) Validate() error { + if cmd.DashboardID == 0 || cmd.UserID == 0 { + return ErrCommandValidationFailed + } + return nil +} + +type UnstarDashboardCommand struct { + UserID int64 `xorm:"user_id"` + DashboardID int64 `xorm:"dashboard_id"` +} + +func (cmd *UnstarDashboardCommand) Validate() error { + if cmd.DashboardID == 0 || cmd.UserID == 0 { + return ErrCommandValidationFailed + } + return nil +} + +// --------------------- +// QUERIES + +type GetUserStarsQuery struct { + UserID int64 `xorm:"user_id"` +} + +type IsStarredByUserQuery struct { + UserID int64 `xorm:"user_id"` + DashboardID int64 `xorm:"dashboard_id"` +} + +type GetUserStarsResult struct { + UserStars map[int64]bool +} diff --git a/pkg/services/star/star.go b/pkg/services/star/star.go new file mode 100644 index 00000000000..b0b363f8741 --- /dev/null +++ b/pkg/services/star/star.go @@ -0,0 +1,12 @@ +package star + +import ( + "context" +) + +type Service interface { + Add(ctx context.Context, cmd *StarDashboardCommand) error + Delete(ctx context.Context, cmd *UnstarDashboardCommand) error + IsStarredByUser(ctx context.Context, query *IsStarredByUserQuery) (bool, error) + GetByUser(ctx context.Context, cmd *GetUserStarsQuery) (*GetUserStarsResult, error) +} diff --git a/pkg/services/star/starimpl/star.go b/pkg/services/star/starimpl/star.go new file mode 100644 index 00000000000..ce09dc5baab --- /dev/null +++ b/pkg/services/star/starimpl/star.go @@ -0,0 +1,42 @@ +package starimpl + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/sqlstore/db" + "github.com/grafana/grafana/pkg/services/star" +) + +type Service struct { + store store +} + +func ProvideService(db db.DB) *Service { + return &Service{ + store: &sqlStore{ + db: db, + }, + } +} + +func (s *Service) Add(ctx context.Context, cmd *star.StarDashboardCommand) error { + if err := cmd.Validate(); err != nil { + return err + } + return s.store.Insert(ctx, cmd) +} + +func (s *Service) Delete(ctx context.Context, cmd *star.UnstarDashboardCommand) error { + if err := cmd.Validate(); err != nil { + return err + } + return s.store.Delete(ctx, cmd) +} + +func (s *Service) IsStarredByUser(ctx context.Context, query *star.IsStarredByUserQuery) (bool, error) { + return s.store.Get(ctx, query) +} + +func (s *Service) GetByUser(ctx context.Context, cmd *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) { + return s.store.List(ctx, cmd) +} diff --git a/pkg/services/star/starimpl/store.go b/pkg/services/star/starimpl/store.go new file mode 100644 index 00000000000..9fe3c2248d8 --- /dev/null +++ b/pkg/services/star/starimpl/store.go @@ -0,0 +1,69 @@ +package starimpl + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/db" + "github.com/grafana/grafana/pkg/services/star" +) + +type store interface { + Get(ctx context.Context, query *star.IsStarredByUserQuery) (bool, error) + Insert(ctx context.Context, cmd *star.StarDashboardCommand) error + Delete(ctx context.Context, cmd *star.UnstarDashboardCommand) error + List(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) +} + +type sqlStore struct { + db db.DB +} + +func (s *sqlStore) Get(ctx context.Context, query *star.IsStarredByUserQuery) (bool, error) { + var isStarred bool + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + rawSQL := "SELECT 1 from star where user_id=? and dashboard_id=?" + results, err := sess.Query(rawSQL, query.UserID, query.DashboardID) + + if err != nil { + return err + } + + isStarred = len(results) != 0 + return nil + }) + return isStarred, err +} + +func (s *sqlStore) Insert(ctx context.Context, cmd *star.StarDashboardCommand) error { + return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + entity := star.Star{ + UserID: cmd.UserID, + DashboardID: cmd.DashboardID, + } + + _, err := sess.Insert(&entity) + return err + }) +} + +func (s *sqlStore) Delete(ctx context.Context, cmd *star.UnstarDashboardCommand) error { + return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + var rawSQL = "DELETE FROM star WHERE user_id=? and dashboard_id=?" + _, err := sess.Exec(rawSQL, cmd.UserID, cmd.DashboardID) + return err + }) +} + +func (s *sqlStore) List(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) { + userStars := make(map[int64]bool) + err := s.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + var stars = make([]star.Star, 0) + err := dbSession.Where("user_id=?", query.UserID).Find(&stars) + for _, star := range stars { + userStars[star.DashboardID] = true + } + return err + }) + return &star.GetUserStarsResult{UserStars: userStars}, err +} diff --git a/pkg/services/star/starimpl/store_test.go b/pkg/services/star/starimpl/store_test.go new file mode 100644 index 00000000000..c4efeed0823 --- /dev/null +++ b/pkg/services/star/starimpl/store_test.go @@ -0,0 +1,60 @@ +//go:build integration +// +build integration + +package starimpl + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/star" + "github.com/stretchr/testify/require" +) + +func TestUserStarsDataAccess(t *testing.T) { + t.Run("Testing User Stars Data Access", func(t *testing.T) { + ss := sqlstore.InitTestDB(t) + starStore := sqlStore{db: ss} + + t.Run("Given saved star", func(t *testing.T) { + cmd := star.StarDashboardCommand{ + DashboardID: 10, + UserID: 12, + } + err := starStore.Insert(context.Background(), &cmd) + require.NoError(t, err) + + t.Run("IsStarredByUser should return true when starred", func(t *testing.T) { + query := star.IsStarredByUserQuery{UserID: 12, DashboardID: 10} + isStarred, err := starStore.Get(context.Background(), &query) + require.NoError(t, err) + require.True(t, isStarred) + }) + + t.Run("IsStarredByUser should return false when not starred", func(t *testing.T) { + query := star.IsStarredByUserQuery{UserID: 12, DashboardID: 12} + isStarred, err := starStore.Get(context.Background(), &query) + require.NoError(t, err) + require.False(t, isStarred) + }) + + t.Run("List should return a list of size 1", func(t *testing.T) { + query := star.GetUserStarsQuery{UserID: 12} + result, err := starStore.List(context.Background(), &query) + require.NoError(t, err) + require.Equal(t, 1, len(result.UserStars)) + }) + + t.Run("Delete should remove the star", func(t *testing.T) { + deleteQuery := star.UnstarDashboardCommand{DashboardID: 10, UserID: 12} + err := starStore.Delete(context.Background(), &deleteQuery) + require.NoError(t, err) + getQuery := star.IsStarredByUserQuery{UserID: 12, DashboardID: 10} + isStarred, err := starStore.Get(context.Background(), &getQuery) + require.NoError(t, err) + require.False(t, isStarred) + }) + }) + }) +} diff --git a/pkg/services/star/startest/fake.go b/pkg/services/star/startest/fake.go new file mode 100644 index 00000000000..e020a360be1 --- /dev/null +++ b/pkg/services/star/startest/fake.go @@ -0,0 +1,33 @@ +package startest + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/star" +) + +type FakeStarService struct { + ExpectedStars *star.Star + ExpectedError error + ExpectedUserStars *star.GetUserStarsResult +} + +func NewStarServiceFake() *FakeStarService { + return &FakeStarService{} +} + +func (f *FakeStarService) IsStarredByUser(ctx context.Context, query *star.IsStarredByUserQuery) (bool, error) { + return true, f.ExpectedError +} + +func (f *FakeStarService) Add(ctx context.Context, cmd *star.StarDashboardCommand) error { + return f.ExpectedError +} + +func (f *FakeStarService) Delete(ctx context.Context, cmd *star.UnstarDashboardCommand) error { + return f.ExpectedError +} + +func (f *FakeStarService) GetByUser(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) { + return f.ExpectedUserStars, f.ExpectedError +}