diff --git a/redash/queries.go b/redash/queries.go new file mode 100644 index 0000000..a0877a8 --- /dev/null +++ b/redash/queries.go @@ -0,0 +1,174 @@ +package redash + +import ( + "encoding/json" + "io/ioutil" + "net/url" + "strconv" + "time" +) + +// QueriesList models the response from Redash's /api/queries endpoint +type QueriesList struct { + Count int `json:"count"` + Page int `json:"page"` + PageSize int `json:"page_size"` + Results []struct { + ID int `json:"id,omitempty"` + IsArchived bool `json:"is_archived,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + RetrievedAt time.Time `json:"retrieved_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Query string `json:"query,omitempty"` + QueryHash string `json:"query_hash,omitempty"` + Version int `json:"version,omitempty"` + LastModifiedByID int `json:"last_modified_by_id,omitempty"` + Tags []string `json:"tags,omitempty"` + APIKey string `json:"api_key,omitempty"` + DataSourceID int `json:"data_source_id,omitempty"` + LatestQueryDataID int `json:"latest_query_data_id,omitempty"` + Schedule QuerySchedule `json:"schedule,omitempty"` + User User `json:"user,omitempty"` + IsFavorite bool `json:"is_favorite,omitempty"` + IsDraft bool `json:"is_draft,omitempty"` + IsSafe bool `json:"is_safe,omitempty"` + Runtime float32 `json:"runtime,omitempty"` + Options QueryOptions `json:"options,omitempty"` + } +} + +// Query models the response from Redash's /api/queries endpoint +type Query struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Query string `json:"query,omitempty"` + QueryHash string `json:"query_hash,omitempty"` + Version int `json:"version,omitempty"` + Schedule QuerySchedule `json:"schedule,omitempty"` + APIKey string `json:"api_key,omitempty"` + IsArchived bool `json:"is_archived,omitempty"` + IsDraft bool `json:"is_draft,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + DataSourceID int `json:"data_source_id,omitempty"` + LatestQueryDataID int `json:"latest_query_data_id,omitempty"` + Tags []string `json:"tags,omitempty"` + IsSafe bool `json:"is_safe,omitempty"` + User User `json:"user,omitempty"` + LastModifiedBy User `json:"last_modified_by,omitempty"` + IsFavorite bool `json:"is_favorite,omitempty"` + CanEdit bool `json:"can_edit,omitempty"` + Options QueryOptions `json:"options,omitempty"` + Visualizations []QueryVisualization `json:"visualizations,omitempty"` +} + +// QuerySchedule struct +type QuerySchedule struct { + Interval int `json:"interval,omitempty"` + Time string `json:"time,omitempty"` + DayOfWeek string `json:"day_of_week,omitempty"` + Until interface{} `json:"until,omitempty"` +} + +// QueryOptions struct +type QueryOptions struct { + Parameters []QueryOptionsParameter `json:"parameters,omitempty"` +} + +// QueryOptionsParameter struct +type QueryOptionsParameter struct { + Title string `json:"title,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + EnumOptions string `json:"enum_options,omitempty"` + Locals []interface{} `json:"locals,omitempty"` + Value string `json:"value,omitempty"` +} + +// QueryVisualization struct +type QueryVisualization struct { + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Options QueryVisualizationOptions `json:"options,omitempty"` +} + +// QueryVisualizationOptions struct +type QueryVisualizationOptions struct { + GlobalSeriesType string `json:"global_series_type,omitempty"` + SortX bool `json:"sort_x,omitempty"` + Legend QueryVisualizationLegendOptions `json:"legend,omitempty"` + YAxis []QueryAxisOptions `json:"y_axis,omitempty"` + XAxis QueryAxisOptions `json:"x_axis,omitempty"` +} + +// QueryVisualizationLegendOptions struct +type QueryVisualizationLegendOptions struct { + Enabled bool `json:"enabled"` + Placement string `json:"placement"` +} + +// QueryAxisOptions struct +type QueryAxisOptions struct { + Type string `json:"type,omitempty"` + Opposite bool `json:"opposite,omitempty"` + Labels QueryAxisLabelsOptions `json:"labels,omitempty"` +} + +// QueryAxisLabelsOptions struct +type QueryAxisLabelsOptions struct { + Enabled bool `json:"enabled"` +} + +// GetQueries returns a paginated list of queries +func (c *Client) GetQueries() (*QueriesList, error) { + path := "/api/queries" + + queryParams := url.Values{} + response, err := c.get(path, queryParams) + + if err != nil { + return nil, err + } + body, _ := ioutil.ReadAll(response.Body) + + queries := QueriesList{} + err = json.Unmarshal(body, &queries) + if err != nil { + return nil, err + } + + defer response.Body.Close() + + return &queries, nil +} + +// GetQuery gets a specific query +func (c *Client) GetQuery(id int) (*Query, error) { + path := "/api/queries/" + strconv.Itoa(id) + + queryParams := url.Values{} + response, err := c.get(path, queryParams) + if err != nil { + return nil, err + } + + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + + query := Query{} + + err = json.Unmarshal(body, &query) + if err != nil { + return nil, err + } + + return &query, nil +} diff --git a/redash/queries_test.go b/redash/queries_test.go new file mode 100644 index 0000000..52c8d27 --- /dev/null +++ b/redash/queries_test.go @@ -0,0 +1,86 @@ +package redash + +import ( + "io/ioutil" + "testing" + "time" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestGetQueries(t *testing.T) { + assert := assert.New(t) + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + c, _ := NewClient(&Config{RedashURI: "https://com.acme/", APIKey: "ApIkEyApIkEyApIkEyApIkEyApIkEy"}) + + body, err := ioutil.ReadFile("testdata/get-queries.json") + if err != nil { + panic(err.Error()) + } + httpmock.RegisterResponder("GET", "https://com.acme/api/queries", + httpmock.NewStringResponder(200, string(body))) + + queries, err := c.GetQueries() + assert.Nil(err) + + assert.Equal(3, queries.Count) + assert.Equal(1, queries.Page) + assert.Equal(10, queries.PageSize) + assert.Equal(3, len(queries.Results)) +} + +func TestGetQuery(t *testing.T) { + assert := assert.New(t) + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + c, _ := NewClient(&Config{RedashURI: "https://com.acme/", APIKey: "ApIkEyApIkEyApIkEyApIkEyApIkEy"}) + + body, err := ioutil.ReadFile("testdata/get-query.json") + if err != nil { + panic(err.Error()) + } + httpmock.RegisterResponder("GET", "https://com.acme/api/queries/1", + httpmock.NewStringResponder(200, string(body))) + + query, err := c.GetQuery(1) + assert.Nil(err) + + assert.Equal(1, query.ID) + assert.Equal("Daily Active Users", query.Name) + assert.Equal("Service X DAU", query.Description) + assert.Equal("SELECT 1 + 1;", query.Query) + assert.Equal("ec2fda0cc5a54b38f81744fcad43ce5a", query.QueryHash) + assert.Equal(1, query.Version) + assert.Equal(false, query.IsArchived) + assert.Equal(false, query.IsDraft) + assert.Equal(true, query.IsSafe) + assert.Equal(false, query.IsFavorite) + assert.Equal(false, query.CanEdit) + assert.Equal(2, query.DataSourceID) + expectedUpdateAt, _ := time.Parse(time.RFC3339, "2021-11-07T22:22:34.929Z") + assert.Equal(expectedUpdateAt, query.UpdatedAt) + expectedCreatedAt, _ := time.Parse(time.RFC3339, "2021-08-13T23:29:12.743Z") + assert.Equal(expectedCreatedAt, query.CreatedAt) + + assert.Equal(1, query.User.ID) + assert.Equal("Admin", query.User.Name) + assert.Equal("admin@example.com", query.User.Email) + + assert.Equal(2, query.LastModifiedBy.ID) + assert.Equal("Developer", query.LastModifiedBy.Name) + assert.Equal("developer@example.com", query.LastModifiedBy.Email) + + assert.Equal(2, len(query.Visualizations)) + queryVisualisation1 := query.Visualizations[0] + assert.Equal(1, queryVisualisation1.ID) + assert.Equal("TABLE", queryVisualisation1.Type) + assert.Equal("Table", queryVisualisation1.Name) + queryVisualisation2 := query.Visualizations[1] + assert.Equal(2, queryVisualisation2.ID) + assert.Equal("CHART", queryVisualisation2.Type) + assert.Equal("DAU", queryVisualisation2.Name) +} diff --git a/redash/testdata/get-queries.json b/redash/testdata/get-queries.json new file mode 100644 index 0000000..ad6c46f --- /dev/null +++ b/redash/testdata/get-queries.json @@ -0,0 +1,16 @@ +{ + "count": 3, + "page": 1, + "page_size": 10, + "results": [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + } + ] +} diff --git a/redash/testdata/get-query.json b/redash/testdata/get-query.json new file mode 100644 index 0000000..7e935f4 --- /dev/null +++ b/redash/testdata/get-query.json @@ -0,0 +1,82 @@ +{ + "id": 1, + "latest_query_data_id": 3919563, + "name": "Daily Active Users", + "description": "Service X DAU", + "query": "SELECT 1 + 1;", + "query_hash": "ec2fda0cc5a54b38f81744fcad43ce5a", + "schedule": null, + "api_key": "ApIkEyApIkEyApIkEyApIkEyApIkEy", + "is_archived": false, + "is_draft": false, + "updated_at": "2021-11-07T22:22:34.929Z", + "created_at": "2021-08-13T23:29:12.743Z", + "data_source_id": 2, + "options": { "parameters": [] }, + "version": 1, + "tags": [], + "is_safe": true, + "user": { + "id": 1, + "name": "Admin", + "email": "admin@example.com", + "profile_image_url": "https://www.gravatar.com/avatar/example", + "groups": [2, 1, 3], + "updated_at": "2021-11-02T21:17:39.226Z", + "created_at": "2021-01-21T10:29:52.872Z", + "disabled_at": null, + "is_disabled": false, + "active_at": null, + "is_invitation_pending": false, + "is_email_verified": true, + "auth_type": "password" + }, + "last_modified_by": { + "id": 2, + "name": "Developer", + "email": "developer@example.com", + "profile_image_url": "https://www.gravatar.com/avatar/example", + "groups": [2, 4, 4, 1], + "updated_at": "2021-11-16T23:34:36.829Z", + "created_at": "2021-03-13T23:46:09.191Z", + "disabled_at": null, + "is_disabled": false, + "active_at": "2021-11-02T08:09:03Z", + "is_invitation_pending": false, + "is_email_verified": true, + "auth_type": "password" + }, + "visualizations": [ + { + "id": 1, + "type": "TABLE", + "name": "Table", + "description": "", + "options": {}, + "updated_at": "2021-03-13T23:29:12.747Z", + "created_at": "2021-03-13T23:29:12.747Z" + }, + { + "id": 2, + "type": "CHART", + "name": "DAU", + "description": "", + "options": { + "yAxis": [{ "type": "linear" }, { "type": "linear", "opposite": true }], + "series": { "stacking": null }, + "globalSeriesType": "line", + "sortX": true, + "seriesOptions": { + "DAU": { "zIndex": 0, "index": 0, "type": "line", "yAxis": 0 } + }, + "xAxis": { "labels": { "enabled": true }, "type": "datetime" }, + "columnMapping": { "Date": "x", "DAU": "y" }, + "legend": { "enabled": true } + }, + "updated_at": "2021-03-13T23:30:07.968Z", + "created_at": "2021-03-13T23:30:07.968Z" + } + ], + "is_favorite": false, + "can_edit": false +}