Query history: Create API to delete query from query history (#44653)

* Query history: Add delete and refactor

* Update docs/sources/http_api/query_history.md

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
pull/44921/head
Ivana Huckova 3 years ago committed by GitHub
parent 1680e284e5
commit 0f362f8dfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      docs/sources/http_api/query_history.md
  2. 29
      pkg/services/queryhistory/api.go
  3. 36
      pkg/services/queryhistory/database.go
  4. 41
      pkg/services/queryhistory/models.go
  5. 9
      pkg/services/queryhistory/queryhistory.go
  6. 2
      pkg/services/queryhistory/queryhistory_create_test.go
  7. 23
      pkg/services/queryhistory/queryhistory_delete_test.go
  8. 40
      pkg/services/queryhistory/queryhistory_test.go

@ -49,11 +49,63 @@ JSON body schema:
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
{ {
"message": "Query successfully added to query history", "result": {
"uid": "Ahg678z",
"datasourceUid": "PE1C5CBDA0504A6A3",
"createdBy": 1,
"createdAt": 1643630762,
"starred": false,
"comment": "",
"queries": [
{
"refId": "A",
"key": "Q-87fed8e3-62ba-4eb2-8d2a-4129979bb4de-0",
"scenarioId": "csv_content",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
}
}
]
}
}
```
Status codes:
- **200** – OK
- **400** - Errors (invalid JSON, missing or invalid fields)
- **500** – Unable to add query to the database
### Delete query from Query history by UID
`DELETE /api/query-history/:uid`
Deletes the query in query history that matches the specified uid. It requires that the user is logged in and that Query history feature is enabled in config file.
**Example Request**:
```http
DELETE /api/query-history/P8zM2I1nz HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "Query deleted",
"id": 28
} }
``` ```
Status codes: Status codes:
- **200** – OK - **200** – OK
- **500** – Errors (invalid JSON, missing or invalid fields) - **404** - Query in query history not found
- **500** – Unable to delete query from the database

@ -7,12 +7,14 @@ import (
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
func (s *QueryHistoryService) registerAPIEndpoints() { func (s *QueryHistoryService) registerAPIEndpoints() {
s.RouteRegister.Group("/api/query-history", func(entities routing.RouteRegister) { s.RouteRegister.Group("/api/query-history", func(entities routing.RouteRegister) {
entities.Post("/", middleware.ReqSignedIn, routing.Wrap(s.createHandler)) entities.Post("/", middleware.ReqSignedIn, routing.Wrap(s.createHandler))
entities.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(s.deleteHandler))
}) })
} }
@ -22,10 +24,31 @@ func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Respo
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
} }
err := s.CreateQueryInQueryHistory(c.Req.Context(), c.SignedInUser, cmd) query, err := s.CreateQueryInQueryHistory(c.Req.Context(), c.SignedInUser, cmd)
if err != nil { if err != nil {
return response.Error(500, "Failed to create query history", err) return response.Error(http.StatusInternalServerError, "Failed to create query history", err)
} }
return response.Success("Query successfully added to query history") return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query})
}
func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Response {
queryUID := web.Params(c.Req)[":uid"]
if len(queryUID) == 0 {
return response.Error(http.StatusNotFound, "Query in query history not found", nil)
}
if !util.IsValidShortUID(queryUID) {
return response.Error(http.StatusNotFound, "Query in query history not found", nil)
}
id, err := s.DeleteQueryFromQueryHistory(c.Req.Context(), c.SignedInUser, queryUID)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete query from query history", err)
}
return response.JSON(http.StatusOK, DeleteQueryFromQueryHistoryResponse{
Message: "Query deleted",
ID: id,
})
} }

@ -9,12 +9,12 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func (s QueryHistoryService) createQuery(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error { func (s QueryHistoryService) createQuery(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error) {
queryHistory := QueryHistory{ queryHistory := QueryHistory{
OrgId: user.OrgId, OrgID: user.OrgId,
Uid: util.GenerateShortUID(), UID: util.GenerateShortUID(),
Queries: cmd.Queries, Queries: cmd.Queries,
DatasourceUid: cmd.DatasourceUid, DatasourceUID: cmd.DatasourceUID,
CreatedBy: user.UserId, CreatedBy: user.UserId,
CreatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(),
Comment: "", Comment: "",
@ -25,8 +25,32 @@ func (s QueryHistoryService) createQuery(ctx context.Context, user *models.Signe
return err return err
}) })
if err != nil { if err != nil {
return err return QueryHistoryDTO{}, err
}
dto := QueryHistoryDTO{
UID: queryHistory.UID,
DatasourceUID: queryHistory.DatasourceUID,
CreatedBy: queryHistory.CreatedBy,
CreatedAt: queryHistory.CreatedAt,
Comment: queryHistory.Comment,
Queries: queryHistory.Queries,
Starred: false,
} }
return nil return dto, nil
}
func (s QueryHistoryService) deleteQuery(ctx context.Context, user *models.SignedInUser, UID string) (int64, error) {
var queryID int64
err := s.SQLStore.WithDbSession(ctx, func(session *sqlstore.DBSession) error {
id, err := session.Where("org_id = ? AND created_by = ? AND uid = ?", user.OrgId, user.UserId, UID).Delete(QueryHistory{})
if id == 0 {
return ErrQueryNotFound
}
queryID = id
return err
})
return queryID, err
} }

@ -1,21 +1,48 @@
package queryhistory package queryhistory
import ( import (
"errors"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
) )
var (
ErrQueryNotFound = errors.New("query in query history not found")
)
type QueryHistory struct { type QueryHistory struct {
Id int64 `json:"id"` ID int64 `xorm:"pk autoincr 'id'"`
Uid string `json:"uid"` UID string `xorm:"uid"`
DatasourceUid string `json:"datasourceUid"` DatasourceUID string `xorm:"datasource_uid"`
OrgId int64 `json:"orgId"` OrgID int64 `xorm:"org_id"`
CreatedBy int64
CreatedAt int64
Comment string
Queries *simplejson.Json
}
type CreateQueryInQueryHistoryCommand struct {
DatasourceUID string `json:"datasourceUid"`
Queries *simplejson.Json `json:"queries"`
}
type QueryHistoryDTO struct {
UID string `json:"uid"`
DatasourceUID string `json:"datasourceUid"`
CreatedBy int64 `json:"createdBy"` CreatedBy int64 `json:"createdBy"`
CreatedAt int64 `json:"createdAt"` CreatedAt int64 `json:"createdAt"`
Comment string `json:"comment"` Comment string `json:"comment"`
Queries *simplejson.Json `json:"queries"` Queries *simplejson.Json `json:"queries"`
Starred bool `json:"starred"`
} }
type CreateQueryInQueryHistoryCommand struct { // QueryHistoryResponse is a response struct for QueryHistoryDTO
DatasourceUid string `json:"datasourceUid"` type QueryHistoryResponse struct {
Queries *simplejson.Json `json:"queries"` Result QueryHistoryDTO `json:"result"`
}
// DeleteQueryFromQueryHistoryResponse is the response struct for deleting a query from query history
type DeleteQueryFromQueryHistoryResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
} }

@ -27,7 +27,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, routeRegister
} }
type Service interface { type Service interface {
CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error)
DeleteQueryFromQueryHistory(ctx context.Context, user *models.SignedInUser, UID string) (int64, error)
} }
type QueryHistoryService struct { type QueryHistoryService struct {
@ -37,6 +38,10 @@ type QueryHistoryService struct {
log log.Logger log log.Logger
} }
func (s QueryHistoryService) CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error { func (s QueryHistoryService) CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error) {
return s.createQuery(ctx, user, cmd) return s.createQuery(ctx, user, cmd)
} }
func (s QueryHistoryService) DeleteQueryFromQueryHistory(ctx context.Context, user *models.SignedInUser, UID string) (int64, error) {
return s.deleteQuery(ctx, user, UID)
}

@ -11,7 +11,7 @@ func TestCreateQueryInQueryHistory(t *testing.T) {
testScenario(t, "When users tries to create query in query history it should succeed", testScenario(t, "When users tries to create query in query history it should succeed",
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
command := CreateQueryInQueryHistoryCommand{ command := CreateQueryInQueryHistoryCommand{
DatasourceUid: "NCzh67i", DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{ Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test", "expr": "test",
}), }),

@ -0,0 +1,23 @@
package queryhistory
import (
"testing"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
)
func TestDeleteQueryFromQueryHistory(t *testing.T) {
testScenarioWithQueryInQueryHistory(t, "When users tries to delete query in query history that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
testScenarioWithQueryInQueryHistory(t, "When users tries to delete query in query history that exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
})
}

@ -9,6 +9,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -22,10 +24,11 @@ var (
) )
type scenarioContext struct { type scenarioContext struct {
ctx *web.Context ctx *web.Context
service *QueryHistoryService service *QueryHistoryService
reqContext *models.ReqContext reqContext *models.ReqContext
sqlStore *sqlstore.SQLStore sqlStore *sqlstore.SQLStore
initialResult QueryHistoryResponse
} }
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
@ -71,7 +74,36 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
}) })
} }
func testScenarioWithQueryInQueryHistory(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
command := CreateQueryInQueryHistoryCommand{
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test",
}),
}
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
sc.initialResult = validateAndUnMarshalResponse(t, resp)
fn(t, sc)
})
}
func mockRequestBody(v interface{}) io.ReadCloser { func mockRequestBody(v interface{}) io.ReadCloser {
b, _ := json.Marshal(v) b, _ := json.Marshal(v)
return io.NopCloser(bytes.NewReader(b)) return io.NopCloser(bytes.NewReader(b))
} }
func validateAndUnMarshalResponse(t *testing.T, resp response.Response) QueryHistoryResponse {
t.Helper()
require.Equal(t, 200, resp.Status())
var result = QueryHistoryResponse{}
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
return result
}

Loading…
Cancel
Save