Skip to content

Commit 408fb4e

Browse files
authored
Merge pull request #25 from boatilus/transform-as-filter
Reassign transformer methods to FilterBuilder
2 parents 8440d1f + 847cc1f commit 408fb4e

File tree

3 files changed

+235
-88
lines changed

3 files changed

+235
-88
lines changed

filterbuilder.go

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"regexp"
7+
"strconv"
78
"strings"
89
)
910

@@ -17,18 +18,18 @@ type FilterBuilder struct {
1718
params map[string]string
1819
}
1920

20-
// ExecuteString runs the Postgrest query, returning the result as a JSON
21+
// ExecuteString runs the PostgREST query, returning the result as a JSON
2122
// string.
2223
func (f *FilterBuilder) ExecuteString() (string, countType, error) {
2324
return executeString(f.client, f.method, f.body, []string{f.tableName}, f.headers, f.params)
2425
}
2526

26-
// Execute runs the Postgrest query, returning the result as a byte slice.
27+
// Execute runs the PostgREST query, returning the result as a byte slice.
2728
func (f *FilterBuilder) Execute() ([]byte, countType, error) {
2829
return execute(f.client, f.method, f.body, []string{f.tableName}, f.headers, f.params)
2930
}
3031

31-
// ExecuteTo runs the Postgrest query, encoding the result to the supplied
32+
// ExecuteTo runs the PostgREST query, encoding the result to the supplied
3233
// interface. Note that the argument for the to parameter should always be a
3334
// reference to a slice.
3435
func (f *FilterBuilder) ExecuteTo(to interface{}) (countType, error) {
@@ -221,3 +222,79 @@ func (f *FilterBuilder) TextSearch(column, userQuery, config, tsType string) *Fi
221222
f.params[column] = typePart + "fts" + configPart + "." + userQuery
222223
return f
223224
}
225+
226+
// OrderOpts describes the options to be provided to Order.
227+
type OrderOpts struct {
228+
Ascending bool
229+
NullsFirst bool
230+
ForeignTable string
231+
}
232+
233+
// DefaultOrderOpts is the default set of options used by Order.
234+
var DefaultOrderOpts = OrderOpts{
235+
Ascending: false,
236+
NullsFirst: false,
237+
ForeignTable: "",
238+
}
239+
240+
// Limits the result to the specified count.
241+
func (f *FilterBuilder) Limit(count int, foreignTable string) *FilterBuilder {
242+
if foreignTable != "" {
243+
f.params[foreignTable+".limit"] = strconv.Itoa(count)
244+
} else {
245+
f.params["limit"] = strconv.Itoa(count)
246+
}
247+
248+
return f
249+
}
250+
251+
// Orders the result with the specified column. A pointer to an OrderOpts
252+
// object can be supplied to specify ordering options.
253+
func (f *FilterBuilder) Order(column string, opts *OrderOpts) *FilterBuilder {
254+
if opts == nil {
255+
opts = &DefaultOrderOpts
256+
}
257+
258+
key := "order"
259+
if opts.ForeignTable != "" {
260+
key = opts.ForeignTable + ".order"
261+
}
262+
263+
ascendingString := "desc"
264+
if opts.Ascending {
265+
ascendingString = "asc"
266+
}
267+
268+
nullsString := "nullslast"
269+
if opts.NullsFirst {
270+
nullsString = "nullsfirst"
271+
}
272+
273+
existingOrder, ok := f.params[key]
274+
if ok && existingOrder != "" {
275+
f.params[key] = fmt.Sprintf("%s,%s.%s.%s", existingOrder, column, ascendingString, nullsString)
276+
} else {
277+
f.params[key] = fmt.Sprintf("%s.%s.%s", column, ascendingString, nullsString)
278+
}
279+
280+
return f
281+
}
282+
283+
// Limits the result to rows within the specified range, inclusive.
284+
func (f *FilterBuilder) Range(from, to int, foreignTable string) *FilterBuilder {
285+
if foreignTable != "" {
286+
f.params[foreignTable+".offset"] = strconv.Itoa(from)
287+
f.params[foreignTable+".limit"] = strconv.Itoa(to - from + 1)
288+
} else {
289+
f.params["offset"] = strconv.Itoa(from)
290+
f.params["limit"] = strconv.Itoa(to - from + 1)
291+
}
292+
return f
293+
}
294+
295+
// Retrieves only one row from the result. The total result set must be one row
296+
// (e.g., by using Limit). Otherwise, this will result in an error.
297+
func (f *FilterBuilder) Single() *FilterBuilder {
298+
f.headers["Accept"] = "application/vnd.pgrst.object+json"
299+
return f
300+
}

filterbuilder_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package postgrest
22

33
import (
4+
"encoding/json"
45
"net/http"
6+
"sort"
57
"testing"
68

79
"github.com/jarcoal/httpmock"
@@ -87,3 +89,156 @@ func ExampleFilterBuilder_ExecuteTo() {
8789
// be the exact number of rows in the users table.
8890
}
8991
}
92+
93+
func TestFilterBuilder_Limit(t *testing.T) {
94+
c := createClient(t)
95+
assert := assert.New(t)
96+
97+
want := []map[string]interface{}{users[0]}
98+
got := []map[string]interface{}{}
99+
100+
if mockResponses {
101+
httpmock.Activate()
102+
defer httpmock.DeactivateAndReset()
103+
104+
httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) {
105+
resp, _ := httpmock.NewJsonResponse(200, want)
106+
resp.Header.Add("Content-Range", "*/2")
107+
return resp, nil
108+
})
109+
}
110+
111+
bs, count, err := c.From("users").Select("id, name, email", "exact", false).Limit(1, "").Execute()
112+
assert.NoError(err)
113+
114+
err = json.Unmarshal(bs, &got)
115+
assert.NoError(err)
116+
assert.EqualValues(want, got)
117+
118+
// Matching supabase-js, the count returned is not the number of transformed
119+
// rows, but the number of filtered rows.
120+
assert.Equal(countType(len(users)), count, "expected count to be %v", len(users))
121+
}
122+
123+
func TestFilterBuilder_Order(t *testing.T) {
124+
c := createClient(t)
125+
assert := assert.New(t)
126+
127+
want := make([]map[string]interface{}, len(users))
128+
copy(want, users)
129+
130+
sort.Slice(want, func(i, j int) bool {
131+
return j < i
132+
})
133+
134+
got := []map[string]interface{}{}
135+
136+
if mockResponses {
137+
httpmock.Activate()
138+
defer httpmock.DeactivateAndReset()
139+
140+
httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) {
141+
resp, _ := httpmock.NewJsonResponse(200, want)
142+
resp.Header.Add("Content-Range", "*/2")
143+
return resp, nil
144+
})
145+
}
146+
147+
bs, count, err := c.
148+
From("users").
149+
Select("id, name, email", "exact", false).
150+
Order("name", &OrderOpts{Ascending: true}).
151+
Execute()
152+
assert.NoError(err)
153+
154+
err = json.Unmarshal(bs, &got)
155+
assert.NoError(err)
156+
assert.EqualValues(want, got)
157+
assert.Equal(countType(len(users)), count)
158+
}
159+
160+
func TestFilterBuilder_Range(t *testing.T) {
161+
c := createClient(t)
162+
assert := assert.New(t)
163+
164+
want := users
165+
got := []map[string]interface{}{}
166+
167+
if mockResponses {
168+
httpmock.Activate()
169+
defer httpmock.DeactivateAndReset()
170+
171+
httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) {
172+
resp, _ := httpmock.NewJsonResponse(200, want)
173+
resp.Header.Add("Content-Range", "*/2")
174+
return resp, nil
175+
})
176+
}
177+
178+
bs, count, err := c.
179+
From("users").
180+
Select("id, name, email", "exact", false).
181+
Range(0, 1, "").
182+
Execute()
183+
assert.NoError(err)
184+
185+
err = json.Unmarshal(bs, &got)
186+
assert.NoError(err)
187+
assert.EqualValues(want, got)
188+
assert.Equal(countType(len(users)), count)
189+
}
190+
191+
func TestFilterBuilder_Single(t *testing.T) {
192+
c := createClient(t)
193+
assert := assert.New(t)
194+
195+
want := users[0]
196+
got := make(map[string]interface{})
197+
198+
t.Run("ValidResult", func(t *testing.T) {
199+
if mockResponses {
200+
httpmock.Activate()
201+
defer httpmock.DeactivateAndReset()
202+
203+
httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) {
204+
resp, _ := httpmock.NewJsonResponse(200, want)
205+
resp.Header.Add("Content-Range", "*/2")
206+
return resp, nil
207+
})
208+
}
209+
210+
bs, count, err := c.
211+
From("users").
212+
Select("id, name, email", "exact", false).
213+
Limit(1, "").
214+
Single().
215+
Execute()
216+
assert.NoError(err)
217+
218+
err = json.Unmarshal(bs, &got)
219+
assert.NoError(err)
220+
assert.EqualValues(want, got)
221+
assert.Equal(countType(len(users)), count)
222+
})
223+
224+
// An error will be returned from PostgREST if the total count of the result
225+
// set > 1, so Single can pretty easily err.
226+
t.Run("Error", func(t *testing.T) {
227+
if mockResponses {
228+
httpmock.Activate()
229+
defer httpmock.DeactivateAndReset()
230+
231+
httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) {
232+
resp, _ := httpmock.NewJsonResponse(500, ExecuteError{
233+
Message: "error message",
234+
})
235+
236+
resp.Header.Add("Content-Range", "*/2")
237+
return resp, nil
238+
})
239+
}
240+
241+
_, _, err := c.From("users").Select("*", "", false).Single().Execute()
242+
assert.Error(err)
243+
})
244+
}

transformbuilder.go

Lines changed: 0 additions & 85 deletions
This file was deleted.

0 commit comments

Comments
 (0)