Skip to content

Commit

Permalink
Merge pull request #25 from boatilus/transform-as-filter
Browse files Browse the repository at this point in the history
Reassign transformer methods to FilterBuilder
  • Loading branch information
yusufpapurcu authored Jan 31, 2022
2 parents 8440d1f + 847cc1f commit 408fb4e
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 88 deletions.
83 changes: 80 additions & 3 deletions filterbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
)

Expand All @@ -17,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) {
Expand Down Expand Up @@ -221,3 +222,79 @@ 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: "",
}

// 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)
} else {
f.params["limit"] = strconv.Itoa(count)
}

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
}

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
}

// 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)
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
}

// 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
}
155 changes: 155 additions & 0 deletions filterbuilder_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package postgrest

import (
"encoding/json"
"net/http"
"sort"
"testing"

"github.com/jarcoal/httpmock"
Expand Down Expand Up @@ -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)
})
}
85 changes: 0 additions & 85 deletions transformbuilder.go

This file was deleted.

0 comments on commit 408fb4e

Please sign in to comment.