Skip to content

Commit

Permalink
Merge pull request #24 from boatilus/update-execute-to
Browse files Browse the repository at this point in the history
Update ExecuteTo functions to return count

New tests added

Comment typos fixed and added more

Readme typos fixed
  • Loading branch information
yusufpapurcu authored Jan 19, 2022
2 parents 0b61da4 + 37ba02b commit 54ad27d
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 23 deletions.
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Postgrest GO

[![golangci-lint](https://github.com/supabase/postgrest-go/actions/workflows/golangci.yml/badge.svg)](https://github.com/supabase/postgrest-go/actions/workflows/golangci.yml) [![CodeFactor](https://www.codefactor.io/repository/github/supabase/postgrest-go/badge/main?s=101cab44de33934fd85cadcd9a9b535a05791670)](https://www.codefactor.io/repository/github/supabase/postgrest-go/overview/main)
[![golangci-lint](https://github.com/supabase/postgrest-go/actions/workflows/golangci.yml/badge.svg)](https://github.com/supabase/postgrest-go/actions/workflows/golangci.yml) [![CodeFactor](https://www.codefactor.io/repository/github/supabase-community/postgrest-go/badge/main?s=101cab44de33934fd85cadcd9a9b535a05791670)](https://www.codefactor.io/repository/github/supabase/postgrest-go/overview/main)

Golang client for [PostgREST](https://postgrest.org). The goal of this library is to make an "ORM-like" restful interface.

Expand Down Expand Up @@ -32,29 +32,40 @@ func main() {
if client.ClientError != nil {
panic(client.ClientError)
}

result := client.Rpc("add_them", "", map[string]int{"a": 12, "b": 3})
if client.ClientError != nil {
panic(client.ClientError)
}

fmt.Println(result)
}
```

- select(): https://supabase.io/docs/reference/javascript/select
- insert(): https://supabase.io/docs/reference/javascript/insert
- update(): https://supabase.io/docs/reference/javascript/update
- delete(): https://supabase.io/docs/reference/javascript/delete
- select(): https://supabase.com/docs/reference/javascript/select
- insert(): https://supabase.com/docs/reference/javascript/insert
- update(): https://supabase.com/docs/reference/javascript/update
- upsert(): https://supabase.com/docs/reference/javascript/upsert
- delete(): https://supabase.com/docs/reference/javascript/delete

## Testing

Some tests are implemented to run against mocked Postgrest endpoints. Optionally, tests can be run against an actual Postgrest instance by setting a `POSTGREST_URL` environment variable to the fully-qualified URL to a Postgrest instance, and, optionally, an `API_KEY` environment variable (if, for example, testing against a local Supabase instance).

A [script](test/seed.sql) is included in the test directory that can be used to seed the test database.

To run all tests:

```bash
go test ./...
```

## License

This repo is liscenced under Apache License.
This repo is licensed under the [Apache License](LICENSE).

## Sponsors

We are building the features of Firebase using enterprise-grade, open source products. We support existing communities wherever possible, and if the products don’t exist we build them and open source them ourselves. Thanks to these sponsors who are making the OSS ecosystem better for everyone.

[![New Sponsor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase)

![Watch this repo](https://gitcdn.xyz/repo/supabase/monorepo/master/web/static/watch-repo.gif "Watch this repo")
12 changes: 9 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
)

var (
version = "v0.0.3"
version = "v0.0.6"
)

// NewClient constructs a new client given a URL to a Postgrest instance.
func NewClient(rawURL, schema string, headers map[string]string) *Client {
// Create URL from rawURL
baseURL, err := url.Parse(rawURL)
Expand Down Expand Up @@ -41,7 +42,7 @@ func NewClient(rawURL, schema string, headers map[string]string) *Client {
c.clientTransport.header.Set("Content-Profile", schema)
c.clientTransport.header.Set("X-Client-Info", "postgrest-go/"+version)

// Set optional headers if exist
// Set optional headers if they exist
for key, value := range headers {
c.clientTransport.header.Set(key, value)
}
Expand All @@ -55,24 +56,29 @@ type Client struct {
clientTransport transport
}

// TokenAuth sets authorization headers for subsequent requests.
func (c *Client) TokenAuth(token string) *Client {
c.clientTransport.header.Set("Authorization", "Basic "+token)
c.clientTransport.header.Set("apikey", token)
return c
}

// ChangeSchema modifies the schema for subsequent requests.
func (c *Client) ChangeSchema(schema string) *Client {
c.clientTransport.header.Set("Accept-Profile", schema)
c.clientTransport.header.Set("Content-Profile", schema)
return c
}

// From sets the table to query from.
func (c *Client) From(table string) *QueryBuilder {
return &QueryBuilder{client: c, tableName: table, headers: map[string]string{}, params: map[string]string{}}
}

// Rpc executes a Postgres function (a.k.a., Remote Prodedure Call), given the
// function name and, optionally, a body, returning the result as a string.
func (c *Client) Rpc(name string, count string, rpcBody interface{}) string {
// Get body if exist
// Get body if it exists
var byteBody []byte = nil
if rpcBody != nil {
jsonBody, err := json.Marshal(rpcBody)
Expand Down
11 changes: 6 additions & 5 deletions execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
"strings"
)

// countType is the integer type returned from execute functions when a count
// specifier is supplied to a builder.
type countType = int64

// ExecuteError is the error response format from postgrest. We really
// only use Code and Message, but we'll keep it as a struct for now.

type ExecuteError struct {
Hint string `json:"hint"`
Details string `json:"details"`
Expand Down Expand Up @@ -93,15 +94,15 @@ func execute(client *Client, method string, body []byte, urlFragments []string,
return executeHelper(client, method, body, urlFragments, headers, params)
}

func executeTo(client *Client, method string, body []byte, to interface{}, urlFragments []string, headers map[string]string, params map[string]string) error {
resp, _, err := executeHelper(client, method, body, urlFragments, headers, params)
func executeTo(client *Client, method string, body []byte, to interface{}, urlFragments []string, headers map[string]string, params map[string]string) (countType, error) {
resp, count, err := executeHelper(client, method, body, urlFragments, headers, params)

if err != nil {
return err
return count, err
}

readableRes := bytes.NewBuffer(resp)

err = json.NewDecoder(readableRes).Decode(&to)
return err
return count, err
}
6 changes: 6 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ var mockResponses bool = false

var mockPath *regexp.Regexp

type TestResult struct {
ID float64 `json:"id"`
Name string `name:"sean"`
Email string `email:"[email protected]"`
}

// A mock table/result set.
var users = []map[string]interface{}{
{
Expand Down
15 changes: 13 additions & 2 deletions filterbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,31 @@ import (
"strings"
)

// FilterBuilder describes a builder for a filtered result set.
type FilterBuilder struct {
client *Client
method string
method string // One of "HEAD", "GET", "POST", "PUT", "DELETE"
body []byte
tableName string
headers map[string]string
params map[string]string
}

// 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.
func (f *FilterBuilder) Execute() ([]byte, countType, error) {
return execute(f.client, f.method, f.body, []string{f.tableName}, f.headers, f.params)
}

func (f *FilterBuilder) ExecuteTo(to interface{}) error {
// 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) {
return executeTo(f.client, f.method, f.body, to, []string{f.tableName}, f.headers, f.params)
}

Expand All @@ -39,6 +46,8 @@ func isOperator(value string) bool {
return false
}

// Filter adds a filtering operator to the query. For a list of available
// operators, see: https://postgrest.org/en/stable/api.html#operators
func (f *FilterBuilder) Filter(column, operator, value string) *FilterBuilder {
if !isOperator(operator) {
f.client.ClientError = fmt.Errorf("invalid filter operator")
Expand Down Expand Up @@ -190,6 +199,8 @@ func (f *FilterBuilder) Overlaps(column string, value []string) *FilterBuilder {
return f
}

// TextSearch performs a full-text search filter. For more information, see
// https://postgrest.org/en/stable/api.html#fts.
func (f *FilterBuilder) TextSearch(column, userQuery, config, tsType string) *FilterBuilder {
var typePart, configPart string
if tsType == "plain" {
Expand Down
89 changes: 89 additions & 0 deletions filterbuilder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package postgrest

import (
"net/http"
"testing"

"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
)

func TestFilterBuilder_ExecuteTo(t *testing.T) {
assert := assert.New(t)
c := createClient(t)

t.Run("ValidResult", func(t *testing.T) {
want := []TestResult{
{
ID: float64(1),
Name: "sean",
Email: "[email protected]",
},
}

var got []TestResult

if mockResponses {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

responder, _ := httpmock.NewJsonResponder(200, []map[string]interface{}{
users[0],
})
httpmock.RegisterRegexpResponder("GET", mockPath, responder)
}

count, err := c.From("users").Select("id, name, email", "", false).Eq("name", "sean").ExecuteTo(&got)
assert.NoError(err)
assert.EqualValues(want, got)
assert.Equal(countType(0), count)
})

t.Run("WithCount", func(t *testing.T) {
want := []TestResult{
{
ID: float64(1),
Name: "sean",
Email: "[email protected]",
},
}

var got []TestResult

if mockResponses {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterRegexpResponder("GET", mockPath, func(req *http.Request) (*http.Response, error) {
resp, _ := httpmock.NewJsonResponse(200, []map[string]interface{}{
users[0],
})

resp.Header.Add("Content-Range", "0-1/1")
return resp, nil
})
}

count, err := c.From("users").Select("id, name, email", "exact", false).Eq("name", "sean").ExecuteTo(&got)
assert.NoError(err)
assert.EqualValues(want, got)
assert.Equal(countType(1), count)
})
}

func ExampleFilterBuilder_ExecuteTo() {
// Given a database with a "users" table containing "id", "name" and "email"
// columns:
var res []struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

client := NewClient("http://localhost:3000", "", nil)
count, err := client.From("users").Select("*", "exact", false).ExecuteTo(&res)
if err == nil && count > 0 {
// The value for res will contain all columns for all users, and count will
// be the exact number of rows in the users table.
}
}
17 changes: 15 additions & 2 deletions querybuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"
)

// QueryBuilder describes a builder for a query.
type QueryBuilder struct {
client *Client
method string
Expand All @@ -15,18 +16,25 @@ type QueryBuilder struct {
params map[string]string
}

// ExecuteString runs the Postgrest query, returning the result as a JSON
// string.
func (q *QueryBuilder) ExecuteString() (string, countType, error) {
return executeString(q.client, q.method, q.body, []string{q.tableName}, q.headers, q.params)
}

// Execute runs the Postgrest query, returning the result as a byte slice.
func (q *QueryBuilder) Execute() ([]byte, countType, error) {
return execute(q.client, q.method, q.body, []string{q.tableName}, q.headers, q.params)
}

func (q *QueryBuilder) ExecuteTo(to interface{}) error {
// 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 (q *QueryBuilder) ExecuteTo(to interface{}) (countType, error) {
return executeTo(q.client, q.method, q.body, to, []string{q.tableName}, q.headers, q.params)
}

// Select performs vertical filtering.
func (q *QueryBuilder) Select(columns, count string, head bool) *FilterBuilder {
if head {
q.method = "HEAD"
Expand Down Expand Up @@ -63,6 +71,7 @@ func (q *QueryBuilder) Select(columns, count string, head bool) *FilterBuilder {
return &FilterBuilder{client: q.client, method: q.method, body: q.body, tableName: q.tableName, headers: q.headers, params: q.params}
}

// Insert performs an insertion into the table.
func (q *QueryBuilder) Insert(value interface{}, upsert bool, onConflict, returning, count string) *FilterBuilder {
q.method = "POST"

Expand Down Expand Up @@ -98,6 +107,8 @@ func (q *QueryBuilder) Insert(value interface{}, upsert bool, onConflict, return
q.body = byteBody
return &FilterBuilder{client: q.client, method: q.method, body: q.body, tableName: q.tableName, headers: q.headers, params: q.params}
}

// Upsert performs an upsert into the table.
func (q *QueryBuilder) Upsert(value interface{}, onConflict, returning, count string) *FilterBuilder {
q.method = "POST"

Expand Down Expand Up @@ -131,6 +142,7 @@ func (q *QueryBuilder) Upsert(value interface{}, onConflict, returning, count st
return &FilterBuilder{client: q.client, method: q.method, body: q.body, tableName: q.tableName, headers: q.headers, params: q.params}
}

// Delete performs a deletion from the table.
func (q *QueryBuilder) Delete(returning, count string) *FilterBuilder {
q.method = "DELETE"

Expand All @@ -148,6 +160,7 @@ func (q *QueryBuilder) Delete(returning, count string) *FilterBuilder {
return &FilterBuilder{client: q.client, method: q.method, body: q.body, tableName: q.tableName, headers: q.headers, params: q.params}
}

// Update performs an update on the table.
func (q *QueryBuilder) Update(value interface{}, returning, count string) *FilterBuilder {
q.method = "PATCH"

Expand All @@ -163,7 +176,7 @@ func (q *QueryBuilder) Update(value interface{}, returning, count string) *Filte
}
q.headers["Prefer"] = strings.Join(headerList, ",")

// Get body if exist
// Get body if it exists
var byteBody []byte = nil
if value != nil {
jsonBody, err := json.Marshal(value)
Expand Down
Loading

0 comments on commit 54ad27d

Please sign in to comment.