From 26cd40e96928ff96437d7b33ce28105b89f5ed29 Mon Sep 17 00:00:00 2001 From: Ron Jones Date: Fri, 21 Jan 2022 13:27:18 -0800 Subject: [PATCH 1/2] Move transformer methods to FilterBuilder --- filterbuilder.go | 71 +++++++++++++++++++++++++++++++++++++ transformbuilder.go | 85 --------------------------------------------- 2 files changed, 71 insertions(+), 85 deletions(-) delete mode 100644 transformbuilder.go diff --git a/filterbuilder.go b/filterbuilder.go index 3c7418a..f40b8f7 100644 --- a/filterbuilder.go +++ b/filterbuilder.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "regexp" + "strconv" "strings" ) @@ -221,3 +222,73 @@ func (f *FilterBuilder) TextSearch(column, userQuery, config, tsType string) *Fi f.params[column] = typePart + "fts" + configPart + "." + userQuery return f } + +// OrderOpts describes the options to be provided to Order. +type OrderOpts struct { + Ascending bool + NullsFirst bool + ForeignTable string +} + +// DefaultOrderOpts is the default set of options used by Order. +var DefaultOrderOpts = OrderOpts{ + Ascending: false, + NullsFirst: false, + ForeignTable: "", +} + +func (f *FilterBuilder) Limit(count int, foreignTable string) *FilterBuilder { + if foreignTable != "" { + f.params[foreignTable+".limit"] = strconv.Itoa(count) + } else { + f.params["limit"] = strconv.Itoa(count) + } + + return f +} + +func (f *FilterBuilder) Order(column string, opts *OrderOpts) *FilterBuilder { + if opts == nil { + opts = &DefaultOrderOpts + } + + key := "order" + if opts.ForeignTable != "" { + key = opts.ForeignTable + ".order" + } + + ascendingString := "desc" + if opts.Ascending { + ascendingString = "asc" + } + + nullsString := "nullslast" + if opts.NullsFirst { + nullsString = "nullsfirst" + } + + existingOrder, ok := f.params[key] + if ok && existingOrder != "" { + f.params[key] = fmt.Sprintf("%s,%s.%s.%s", existingOrder, column, ascendingString, nullsString) + } else { + f.params[key] = fmt.Sprintf("%s.%s.%s", column, ascendingString, nullsString) + } + + return f +} + +func (f *FilterBuilder) Range(from, to int, foreignTable string) *FilterBuilder { + if foreignTable != "" { + f.params[foreignTable+".offset"] = strconv.Itoa(from) + f.params[foreignTable+".limit"] = strconv.Itoa(to - from + 1) + } else { + f.params["offset"] = strconv.Itoa(from) + f.params["limit"] = strconv.Itoa(to - from + 1) + } + return f +} + +func (f *FilterBuilder) Single() *FilterBuilder { + f.headers["Accept"] = "application/vnd.pgrst.object+json" + return f +} diff --git a/transformbuilder.go b/transformbuilder.go deleted file mode 100644 index 03de045..0000000 --- a/transformbuilder.go +++ /dev/null @@ -1,85 +0,0 @@ -package postgrest - -import ( - "fmt" - "strconv" -) - -type TransformBuilder struct { - client *Client - method string - body []byte - headers map[string]string - params map[string]string -} - -func (t *TransformBuilder) ExecuteString() (string, countType, error) { - return executeString(t.client, t.method, t.body, []string{}, t.headers, t.params) -} - -func (t *TransformBuilder) Execute() ([]byte, countType, error) { - return execute(t.client, t.method, t.body, []string{}, t.headers, t.params) -} - -func (t *TransformBuilder) ExecuteTo(to interface{}) (countType, error) { - return executeTo(t.client, t.method, t.body, to, []string{}, t.headers, t.params) -} - -func (t *TransformBuilder) Limit(count int, foreignTable string) *TransformBuilder { - if foreignTable != "" { - t.params[foreignTable+".limit"] = strconv.Itoa(count) - } else { - t.params["limit"] = strconv.Itoa(count) - } - - return t -} - -func (t *TransformBuilder) Order(column, foreignTable string, ascending, nullsFirst bool) *TransformBuilder { - var key string - if foreignTable != "" { - key = foreignTable + ".order" - } else { - key = "order" - } - - existingOrder, ok := t.params[key] - - var ascendingString string - if ascending { - ascendingString = "asc" - } else { - ascendingString = "desc" - } - - var nullsString string - if nullsFirst { - nullsString = "nullsfirst" - } else { - nullsString = "nullslast" - } - - if ok && existingOrder != "" { - t.params[key] = fmt.Sprintf("%s,%s.%s.%s", existingOrder, column, ascendingString, nullsString) - } else { - t.params[key] = fmt.Sprintf("%s.%s.%s", column, ascendingString, nullsString) - } - - return t -} - -func (t *TransformBuilder) Range(from, to int, foreignTable string) *TransformBuilder { - if foreignTable != "" { - t.params[foreignTable+".offset"] = strconv.Itoa(from) - t.params[foreignTable+".limit"] = strconv.Itoa(to - from + 1) - } else { - t.params["offset"] = strconv.Itoa(from) - t.params["limit"] = strconv.Itoa(to - from + 1) - } - return t -} - -func (t *TransformBuilder) Single() *TransformBuilder { - t.headers["Accept"] = "application/vnd.pgrst.object+json" - return t -} From 847cc1feec1f5a21c517691b1922f8a953e97a79 Mon Sep 17 00:00:00 2001 From: Ron Jones Date: Fri, 28 Jan 2022 12:35:28 -0800 Subject: [PATCH 2/2] Add FilterBuilder tests; doc comments --- filterbuilder.go | 12 +++- filterbuilder_test.go | 155 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/filterbuilder.go b/filterbuilder.go index f40b8f7..32dc5ab 100644 --- a/filterbuilder.go +++ b/filterbuilder.go @@ -18,18 +18,18 @@ type FilterBuilder struct { params map[string]string } -// ExecuteString runs the Postgrest query, returning the result as a JSON +// ExecuteString runs the PostgREST query, returning the result as a JSON // string. func (f *FilterBuilder) ExecuteString() (string, countType, error) { return executeString(f.client, f.method, f.body, []string{f.tableName}, f.headers, f.params) } -// Execute runs the Postgrest query, returning the result as a byte slice. +// Execute runs the PostgREST query, returning the result as a byte slice. func (f *FilterBuilder) Execute() ([]byte, countType, error) { return execute(f.client, f.method, f.body, []string{f.tableName}, f.headers, f.params) } -// ExecuteTo runs the Postgrest query, encoding the result to the supplied +// ExecuteTo runs the PostgREST query, encoding the result to the supplied // interface. Note that the argument for the to parameter should always be a // reference to a slice. func (f *FilterBuilder) ExecuteTo(to interface{}) (countType, error) { @@ -237,6 +237,7 @@ var DefaultOrderOpts = OrderOpts{ ForeignTable: "", } +// Limits the result to the specified count. func (f *FilterBuilder) Limit(count int, foreignTable string) *FilterBuilder { if foreignTable != "" { f.params[foreignTable+".limit"] = strconv.Itoa(count) @@ -247,6 +248,8 @@ func (f *FilterBuilder) Limit(count int, foreignTable string) *FilterBuilder { return f } +// Orders the result with the specified column. A pointer to an OrderOpts +// object can be supplied to specify ordering options. func (f *FilterBuilder) Order(column string, opts *OrderOpts) *FilterBuilder { if opts == nil { opts = &DefaultOrderOpts @@ -277,6 +280,7 @@ func (f *FilterBuilder) Order(column string, opts *OrderOpts) *FilterBuilder { return f } +// Limits the result to rows within the specified range, inclusive. func (f *FilterBuilder) Range(from, to int, foreignTable string) *FilterBuilder { if foreignTable != "" { f.params[foreignTable+".offset"] = strconv.Itoa(from) @@ -288,6 +292,8 @@ func (f *FilterBuilder) Range(from, to int, foreignTable string) *FilterBuilder return f } +// Retrieves only one row from the result. The total result set must be one row +// (e.g., by using Limit). Otherwise, this will result in an error. func (f *FilterBuilder) Single() *FilterBuilder { f.headers["Accept"] = "application/vnd.pgrst.object+json" return f diff --git a/filterbuilder_test.go b/filterbuilder_test.go index 1685540..8b76d7f 100644 --- a/filterbuilder_test.go +++ b/filterbuilder_test.go @@ -1,7 +1,9 @@ package postgrest import ( + "encoding/json" "net/http" + "sort" "testing" "github.com/jarcoal/httpmock" @@ -87,3 +89,156 @@ func ExampleFilterBuilder_ExecuteTo() { // be the exact number of rows in the users table. } } + +func TestFilterBuilder_Limit(t *testing.T) { + c := createClient(t) + assert := assert.New(t) + + want := []map[string]interface{}{users[0]} + got := []map[string]interface{}{} + + if mockResponses { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) { + resp, _ := httpmock.NewJsonResponse(200, want) + resp.Header.Add("Content-Range", "*/2") + return resp, nil + }) + } + + bs, count, err := c.From("users").Select("id, name, email", "exact", false).Limit(1, "").Execute() + assert.NoError(err) + + err = json.Unmarshal(bs, &got) + assert.NoError(err) + assert.EqualValues(want, got) + + // Matching supabase-js, the count returned is not the number of transformed + // rows, but the number of filtered rows. + assert.Equal(countType(len(users)), count, "expected count to be %v", len(users)) +} + +func TestFilterBuilder_Order(t *testing.T) { + c := createClient(t) + assert := assert.New(t) + + want := make([]map[string]interface{}, len(users)) + copy(want, users) + + sort.Slice(want, func(i, j int) bool { + return j < i + }) + + got := []map[string]interface{}{} + + if mockResponses { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) { + resp, _ := httpmock.NewJsonResponse(200, want) + resp.Header.Add("Content-Range", "*/2") + return resp, nil + }) + } + + bs, count, err := c. + From("users"). + Select("id, name, email", "exact", false). + Order("name", &OrderOpts{Ascending: true}). + Execute() + assert.NoError(err) + + err = json.Unmarshal(bs, &got) + assert.NoError(err) + assert.EqualValues(want, got) + assert.Equal(countType(len(users)), count) +} + +func TestFilterBuilder_Range(t *testing.T) { + c := createClient(t) + assert := assert.New(t) + + want := users + got := []map[string]interface{}{} + + if mockResponses { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) { + resp, _ := httpmock.NewJsonResponse(200, want) + resp.Header.Add("Content-Range", "*/2") + return resp, nil + }) + } + + bs, count, err := c. + From("users"). + Select("id, name, email", "exact", false). + Range(0, 1, ""). + Execute() + assert.NoError(err) + + err = json.Unmarshal(bs, &got) + assert.NoError(err) + assert.EqualValues(want, got) + assert.Equal(countType(len(users)), count) +} + +func TestFilterBuilder_Single(t *testing.T) { + c := createClient(t) + assert := assert.New(t) + + want := users[0] + got := make(map[string]interface{}) + + t.Run("ValidResult", func(t *testing.T) { + if mockResponses { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) { + resp, _ := httpmock.NewJsonResponse(200, want) + resp.Header.Add("Content-Range", "*/2") + return resp, nil + }) + } + + bs, count, err := c. + From("users"). + Select("id, name, email", "exact", false). + Limit(1, ""). + Single(). + Execute() + assert.NoError(err) + + err = json.Unmarshal(bs, &got) + assert.NoError(err) + assert.EqualValues(want, got) + assert.Equal(countType(len(users)), count) + }) + + // An error will be returned from PostgREST if the total count of the result + // set > 1, so Single can pretty easily err. + t.Run("Error", func(t *testing.T) { + if mockResponses { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) { + resp, _ := httpmock.NewJsonResponse(500, ExecuteError{ + Message: "error message", + }) + + resp.Header.Add("Content-Range", "*/2") + return resp, nil + }) + } + + _, _, err := c.From("users").Select("*", "", false).Single().Execute() + assert.Error(err) + }) +}