| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- package sqlstore
- import (
- "encoding/json"
- "errors"
- "time"
- "github.com/go-xorm/xorm"
- "github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/components/formatter"
- "github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
- diff "github.com/yudai/gojsondiff"
- deltaFormatter "github.com/yudai/gojsondiff/formatter"
- )
- var (
- // ErrUnsupportedDiffType occurs when an invalid diff type is used.
- ErrUnsupportedDiffType = errors.New("sqlstore: unsupported diff type")
- // ErrNilDiff occurs when two compared interfaces are identical.
- ErrNilDiff = errors.New("sqlstore: diff is nil")
- )
- func init() {
- bus.AddHandler("sql", CompareDashboardVersionsCommand)
- bus.AddHandler("sql", GetDashboardVersion)
- bus.AddHandler("sql", GetDashboardVersions)
- bus.AddHandler("sql", RestoreDashboardVersion)
- }
- // CompareDashboardVersionsCommand computes the JSON diff of two versions,
- // assigning the delta of the diff to the `Delta` field.
- func CompareDashboardVersionsCommand(cmd *m.CompareDashboardVersionsCommand) error {
- original, err := getDashboardVersion(cmd.DashboardId, cmd.Original)
- if err != nil {
- return err
- }
- newDashboard, err := getDashboardVersion(cmd.DashboardId, cmd.New)
- if err != nil {
- return err
- }
- left, jsonDiff, err := getDiff(original, newDashboard)
- if err != nil {
- return err
- }
- switch cmd.DiffType {
- case m.DiffDelta:
- deltaOutput, err := deltaFormatter.NewDeltaFormatter().Format(jsonDiff)
- if err != nil {
- return err
- }
- cmd.Delta = []byte(deltaOutput)
- case m.DiffJSON:
- jsonOutput, err := formatter.NewJSONFormatter(left).Format(jsonDiff)
- if err != nil {
- return err
- }
- cmd.Delta = []byte(jsonOutput)
- case m.DiffBasic:
- basicOutput, err := formatter.NewBasicFormatter(left).Format(jsonDiff)
- if err != nil {
- return err
- }
- cmd.Delta = basicOutput
- default:
- return ErrUnsupportedDiffType
- }
- return nil
- }
- // GetDashboardVersion gets the dashboard version for the given dashboard ID
- // and version number.
- func GetDashboardVersion(query *m.GetDashboardVersionCommand) error {
- result, err := getDashboardVersion(query.DashboardId, query.Version)
- if err != nil {
- return err
- }
- query.Result = result
- return nil
- }
- // GetDashboardVersions gets all dashboard versions for the given dashboard ID.
- func GetDashboardVersions(query *m.GetDashboardVersionsCommand) error {
- order := ""
- // the query builder in xorm doesn't provide a way to set
- // a default order, so we perform this check
- if query.OrderBy != "" {
- order = " desc"
- }
- err := x.In("dashboard_id", query.DashboardId).
- OrderBy(query.OrderBy+order).
- Limit(query.Limit, query.Start).
- Find(&query.Result)
- if err != nil {
- return err
- }
- if len(query.Result) < 1 {
- return m.ErrNoVersionsForDashboardId
- }
- return nil
- }
- // RestoreDashboardVersion restores the dashboard data to the given version.
- func RestoreDashboardVersion(cmd *m.RestoreDashboardVersionCommand) error {
- return inTransaction(func(sess *xorm.Session) error {
- // check if dashboard version exists in dashboard_version table
- //
- // normally we could use the getDashboardVersion func here, but since
- // we're in a transaction, we need to run the queries using the
- // session instead of using the global `x`, so we copy those functions
- // here, replacing `x` with `sess`
- dashboardVersion := m.DashboardVersion{}
- has, err := sess.Where(
- "dashboard_id=? AND version=?",
- cmd.DashboardId,
- cmd.Version,
- ).Get(&dashboardVersion)
- if err != nil {
- return err
- }
- if !has {
- return m.ErrDashboardVersionNotFound
- }
- dashboardVersion.Data.Set("id", dashboardVersion.DashboardId)
- // get the dashboard version
- dashboard := m.Dashboard{Id: cmd.DashboardId}
- has, err = sess.Get(&dashboard)
- if err != nil {
- return err
- }
- if has == false {
- return m.ErrDashboardNotFound
- }
- version, err := getMaxVersion(sess, dashboard.Id)
- if err != nil {
- return err
- }
- // revert and save to a new dashboard version
- dashboard.Data = dashboardVersion.Data
- dashboard.Updated = time.Now()
- dashboard.UpdatedBy = cmd.UserId
- dashboard.Version = version
- dashboard.Data.Set("version", dashboard.Version)
- affectedRows, err := sess.Id(dashboard.Id).Update(dashboard)
- if err != nil {
- return err
- }
- if affectedRows == 0 {
- return m.ErrDashboardNotFound
- }
- // save that version a new version
- dashVersion := &m.DashboardVersion{
- DashboardId: dashboard.Id,
- ParentVersion: cmd.Version,
- RestoredFrom: cmd.Version,
- Version: dashboard.Version,
- Created: time.Now(),
- CreatedBy: dashboard.UpdatedBy,
- Message: "",
- Data: dashboard.Data,
- }
- affectedRows, err = sess.Insert(dashVersion)
- if err != nil {
- return err
- }
- if affectedRows == 0 {
- return m.ErrDashboardNotFound
- }
- cmd.Result = &dashboard
- return nil
- })
- }
- // getDashboardVersion is a helper function that gets the dashboard version for
- // the given dashboard ID and version ID.
- func getDashboardVersion(dashboardId int64, version int) (*m.DashboardVersion, error) {
- dashboardVersion := m.DashboardVersion{}
- has, err := x.Where("dashboard_id=? AND version=?", dashboardId, version).Get(&dashboardVersion)
- if err != nil {
- return nil, err
- }
- if !has {
- return nil, m.ErrDashboardVersionNotFound
- }
- dashboardVersion.Data.Set("id", dashboardVersion.DashboardId)
- return &dashboardVersion, nil
- }
- // getDashboard gets a dashboard by ID. Used for retrieving the dashboard
- // associated with dashboard versions.
- func getDashboard(dashboardId int64) (*m.Dashboard, error) {
- dashboard := m.Dashboard{Id: dashboardId}
- has, err := x.Get(&dashboard)
- if err != nil {
- return nil, err
- }
- if has == false {
- return nil, m.ErrDashboardNotFound
- }
- return &dashboard, nil
- }
- // getDiff computes the diff of two dashboard versions.
- func getDiff(originalDash, newDash *m.DashboardVersion) (interface{}, diff.Diff, error) {
- leftBytes, err := simplejson.NewFromAny(originalDash).Encode()
- if err != nil {
- return nil, nil, err
- }
- rightBytes, err := simplejson.NewFromAny(newDash).Encode()
- if err != nil {
- return nil, nil, err
- }
- jsonDiff, err := diff.New().Compare(leftBytes, rightBytes)
- if err != nil {
- return nil, nil, err
- }
- if !jsonDiff.Modified() {
- return nil, nil, ErrNilDiff
- }
- left := make(map[string]interface{})
- err = json.Unmarshal(leftBytes, &left)
- return left, jsonDiff, nil
- }
- type version struct {
- Max int
- }
- // getMaxVersion returns the highest version number in the `dashboard_version`
- // table.
- //
- // This is necessary because sqlite3 doesn't support autoincrement in the same
- // way that Postgres or MySQL do, so we use this to get around that. Since it's
- // impossible to delete a version in Grafana, this is believed to be a
- // safe-enough alternative.
- func getMaxVersion(sess *xorm.Session, dashboardId int64) (int, error) {
- v := version{}
- has, err := sess.Table("dashboard_version").
- Select("MAX(version) AS max").
- Where("dashboard_id = ?", dashboardId).
- Get(&v)
- if !has {
- return 0, m.ErrDashboardNotFound
- }
- if err != nil {
- return 0, err
- }
- v.Max++
- return v.Max, nil
- }
|