commit 2a2e70bfe7e68ca4d5850f3de78515c80c62a62b Author: mehanizm Date: Sun Apr 12 13:05:28 2020 +0300 feat: add and get records methods (initial commit) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d4c6f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +# Created by https://www.gitignore.io/api/visualstudiocode,macos,go +# Edit at https://www.gitignore.io/?templates=visualstudiocode,macos,go + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### VisualStudioCode ### +.vscode/* + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/visualstudiocode,macos,go + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) +coverage.sh \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..901c9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Mike Berezin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0295c9e --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +Golang Airtable API +================ + +[![GoDoc](https://godoc.org/github.com/mehanizm/airtable?status.svg)](https://pkg.go.dev/github.com/mehanizm/airtable) +![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-92%25-brightgreen.svg?longCache=true&style=flat) +![goreportcard](https://goreportcard.com/badge/github.com/mehanizm/airtable) + +A simple #golang package to access the [Airtable API](https://airtable.com/api). diff --git a/client.go b/client.go new file mode 100644 index 0000000..0204484 --- /dev/null +++ b/client.go @@ -0,0 +1,145 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "time" +) + +const ( + airtableBaseURL = "https://api.airtable.com/v0" + rateLimit = 4 +) + +// Client client for airtable api +type Client struct { + client *http.Client + rateLimiter <-chan time.Time + baseURL string + apiKey string +} + +// NewClient airtable client constructor +// your API KEY you can get on your account page +// https://airtable.com/account +func NewClient(apiKey string) *Client { + return &Client{ + client: http.DefaultClient, + rateLimiter: time.Tick(time.Second / time.Duration(rateLimit)), + apiKey: apiKey, + baseURL: airtableBaseURL, + } +} + +// SetRateLimit rate limit setter for custom usage +// Airtable limit is 5 requests per second (we use 4) +// https://airtable.com/{yourDatabaseID}/api/docs#curl/ratelimits +func (at *Client) SetRateLimit(customRateLimit int) { + at.rateLimiter = time.Tick(time.Second / time.Duration(customRateLimit)) +} + +func (at *Client) rateLimit() { + <-at.rateLimiter +} + +func (at *Client) get(db, table, recordID string, params url.Values, target interface{}) error { + at.rateLimit() + url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) + if recordID != "" { + url += fmt.Sprintf("/%s", recordID) + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("cannot create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) + req.URL.RawQuery = params.Encode() + err = at.do(req, url, target) + if err != nil { + return err + } + return nil +} + +func (at *Client) post(db, table string, data, response interface{}) error { + at.rateLimit() + url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) + body, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("cannot marshal body: %w", err) + } + req, err := http.NewRequest("POST", url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("cannot create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) + return at.do(req, url, response) +} + +func (at *Client) delete(db, table string, recordIDs []string, target interface{}) error { + at.rateLimit() + rawURL := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) + params := url.Values{} + for _, recordID := range recordIDs { + params.Set("records[]", recordID) + } + req, err := http.NewRequest("DELETE", rawURL, nil) + if err != nil { + return fmt.Errorf("cannot create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) + req.URL.RawQuery = params.Encode() + err = at.do(req, rawURL, target) + if err != nil { + return err + } + return nil +} + +func (at *Client) patch(db, table, data, response interface{}) error { + at.rateLimit() + url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) + body, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("cannot marshal body: %w", err) + } + req, err := http.NewRequest("PATCH", url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("cannot create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) + return at.do(req, url, response) +} + +func (at *Client) do(req *http.Request, url string, response interface{}) error { + resp, err := at.client.Do(req) + if err != nil { + return fmt.Errorf("HTTP request failure on %s: %w", url, err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return makeHTTPClientError(url, resp) + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("HTTP Read error on response for %s: %w", url, err) + } + err = json.Unmarshal(b, response) + if err != nil { + return fmt.Errorf("JSON decode failed on %s:\n%s\nerror: %w", url, string(b), err) + } + return nil +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..3659b7f --- /dev/null +++ b/client_test.go @@ -0,0 +1,33 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "errors" + "log" + "net/http" + "testing" +) + +func testClient(t *testing.T) *Client { + c := NewClient("apiKey") + c.SetRateLimit(1000) + return c +} + +func TestClient_do(t *testing.T) { + c := testClient(t) + url := mockErrorResponse(404).URL + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatal(err) + } + err = c.do(req, url, nil) + var e *HTTPClientError + if errors.Is(err, e) { + t.Errorf("should be an http error, but was not: %v", err) + } +} diff --git a/coverage_badge.png b/coverage_badge.png new file mode 100644 index 0000000..479126b --- /dev/null +++ b/coverage_badge.png @@ -0,0 +1,7 @@ + +522 Origin Connection Time-out + +

522 Origin Connection Time-out

+
cloudflare-nginx
+ + diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..49f2bf3 --- /dev/null +++ b/errors.go @@ -0,0 +1,36 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "fmt" + "io/ioutil" + "net/http" +) + +// HTTPClientError custom error to handle with response status +type HTTPClientError struct { + StatusCode int + Err error +} + +func (e *HTTPClientError) Error() string { + return fmt.Sprintf("status %d: err %v", e.StatusCode, e.Err) +} + +func makeHTTPClientError(url string, resp *http.Response) error { + var resError error + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + resError = fmt.Errorf("HTTP request failure on %s with status %d\nCannot parse body with: %w", url, resp.StatusCode, err) + } else { + resError = fmt.Errorf("HTTP request failure on %s with status %d\nBody: %v", url, resp.StatusCode, string(body)) + } + return &HTTPClientError{ + StatusCode: resp.StatusCode, + Err: resError, + } +} diff --git a/get-records.go b/get-records.go new file mode 100644 index 0000000..77a4e96 --- /dev/null +++ b/get-records.go @@ -0,0 +1,75 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "fmt" + "net/url" +) + +// GetRecordsConfig helper type to use in +// step by step get records +type GetRecordsConfig struct { + table *Table + params url.Values +} + +// GetRecords prepare step to get records +func (t *Table) GetRecords() *GetRecordsConfig { + return &GetRecordsConfig{ + table: t, + params: url.Values{}, + } +} + +// ReturnFields set returning field names +func (grc *GetRecordsConfig) ReturnFields(fieldNames ...string) *GetRecordsConfig { + for _, fieldName := range fieldNames { + grc.params.Add("fields", fieldName) + } + return grc +} + +// WithFilterFormula add filter to request +func (grc *GetRecordsConfig) WithFilterFormula(filterFormula string) *GetRecordsConfig { + grc.params.Add("filterByFormula", filterFormula) + return grc +} + +// WithSort add sorting to request +func (grc *GetRecordsConfig) WithSort(sortQueries ...struct { + fieldName string + direction string +}) *GetRecordsConfig { + for queryNum, sortQuery := range sortQueries { + grc.params.Add(fmt.Sprintf("sort[%v][field]", queryNum), sortQuery.fieldName) + grc.params.Add(fmt.Sprintf("sort[%v][direction]", queryNum), sortQuery.direction) + } + return grc +} + +// FromView add view parameter to get records +func (grc *GetRecordsConfig) FromView(viewNameOrID string) *GetRecordsConfig { + grc.params.Add("view", viewNameOrID) + return grc +} + +// InStringFormat add parameter to get records in string format +// it require timezone +// https://support.airtable.com/hc/en-us/articles/216141558-Supported-timezones-for-SET-TIMEZONE +// and user locale data +// https://support.airtable.com/hc/en-us/articles/220340268-Supported-locale-modifiers-for-SET-LOCALE +func (grc *GetRecordsConfig) InStringFormat(timeZone, userLocale string) *GetRecordsConfig { + grc.params.Add("cellFormat", "string") + grc.params.Add("timeZone", timeZone) + grc.params.Add("userLocale", userLocale) + return grc +} + +// Do send the prepared get records request +func (grc *GetRecordsConfig) Do() (*Records, error) { + return grc.table.GetRecordsWithParams(grc.params) +} diff --git a/get-records_test.go b/get-records_test.go new file mode 100644 index 0000000..05a84da --- /dev/null +++ b/get-records_test.go @@ -0,0 +1,37 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "testing" +) + +func TestGetRecordsConfig_Do(t *testing.T) { + table := testTable(t) + table.client.baseURL = mockResponse("get_records_with_filter.json").URL + sortQuery1 := struct { + fieldName string + direction string + }{"Field1", "desc"} + sortQuery2 := struct { + fieldName string + direction string + }{"Field2", "asc"} + + records, err := table.GetRecords(). + FromView("view_1"). + WithFilterFormula("AND({Field1}='value_1',NOT({Field2}='value_2'))"). + WithSort(sortQuery1, sortQuery2). + ReturnFields("Field1", "Field2"). + InStringFormat("Europe/Moscow", "ru"). + Do() + if err != nil { + t.Errorf("there should not be an err, but was: %v", err) + } + if len(records.Records) != 3 { + t.Errorf("there should be 3 records, but was %v", len(records.Records)) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..103ee20 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/mehanizm/airtable + +go 1.13 + +require gopkg.in/square/go-jose.v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..244b95a --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo= +gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/mock-response_test.go b/mock-response_test.go new file mode 100644 index 0000000..6f4814a --- /dev/null +++ b/mock-response_test.go @@ -0,0 +1,34 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "path/filepath" +) + +func mockResponse(paths ...string) *httptest.Server { + parts := []string{".", "testdata"} + filename := filepath.Join(append(parts, paths...)...) + + mockData, err := ioutil.ReadFile(filename) + if err != nil { + log.Fatal(err) + } + + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write(mockData) + })) +} + +func mockErrorResponse(code int) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + http.Error(rw, "An error occurred", code) + })) +} diff --git a/record.go b/record.go new file mode 100644 index 0000000..67b8d32 --- /dev/null +++ b/record.go @@ -0,0 +1,65 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import "net/url" + +// Record base time of airtable record fields +type Record struct { + client *Client + table *Table + ID string `json:"id,omitempty"` + Fields map[string]interface{} `json:"fields"` + CreatedTime string `json:"createdTime,omitempty"` + Deleted bool `json:"deleted,omitempty"` +} + +// GetRecord get record from table +// https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:retrieve +func (t *Table) GetRecord(recordID string) (*Record, error) { + result := new(Record) + err := t.client.get(t.dbName, t.tableName, recordID, url.Values{}, result) + if err != nil { + return nil, err + } + result.client = t.client + result.table = t + return result, nil +} + +// UpdateRecordPartial updates partial info on record +func (r *Record) UpdateRecordPartial(changedFields map[string]interface{}) (*Record, error) { + data := &Records{ + Records: []*Record{ + { + ID: r.ID, + Fields: changedFields, + }, + }, + } + response := new(Records) + err := r.client.patch(r.table.dbName, r.table.tableName, data, response) + if err != nil { + return nil, err + } + result := response.Records[0] + result.client = r.client + result.table = r.table + return result, nil +} + +// DeleteRecord delete one record +func (r *Record) DeleteRecord() (*Record, error) { + response := new(Records) + err := r.client.delete(r.table.dbName, r.table.tableName, []string{r.ID}, response) + if err != nil { + return nil, err + } + result := response.Records[0] + result.client = r.client + result.table = r.table + return result, nil +} diff --git a/record_test.go b/record_test.go new file mode 100644 index 0000000..9f1849e --- /dev/null +++ b/record_test.go @@ -0,0 +1,91 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "errors" + "reflect" + "testing" +) + +func TestRecord_GetRecord(t *testing.T) { + table := testTable(t) + table.client.baseURL = mockResponse("get_record.json").URL + record, err := table.GetRecord("recnTq6CsvFM6vX2m") + if err != nil { + t.Error("must be no error") + } + expected := &Record{ + client: table.client, + table: table, + ID: "recnTq6CsvFM6vX2m", + CreatedTime: "2020-04-10T11:30:57.000Z", + Fields: map[string]interface{}{ + "Field1": "Field1", + "Field2": true, + "Field3": "2020-04-06T06:00:00.000Z", + }, + } + if !reflect.DeepEqual(record, expected) { + t.Errorf("expected: %#v\nbut got: %#v\n", expected, record) + } + table.client.baseURL = mockErrorResponse(404).URL + _, err = table.GetRecord("recnTq6CsvFM6vX2m") + var e *HTTPClientError + if errors.Is(err, e) { + t.Errorf("should be an http error, but was not: %v", err) + } +} + +func TestRecord_DeleteRecord(t *testing.T) { + record := testRecord(t) + record.client.baseURL = mockResponse("delete_record.json").URL + res, err := record.DeleteRecord() + if err != nil { + t.Error("must be no error") + } + if !res.Deleted { + t.Errorf("expected that record will be deleted, but was: %#v", record.Deleted) + } + record.client.baseURL = mockErrorResponse(404).URL + _, err = record.DeleteRecord() + var e *HTTPClientError + if errors.Is(err, e) { + t.Errorf("should be an http error, but was not: %v", err) + } +} + +func TestRecord_UpdateRecordPartial(t *testing.T) { + record := testRecord(t) + record.client.baseURL = mockResponse("get_records_with_filter.json").URL + res, err := record.UpdateRecordPartial(map[string]interface{}{"Field_2": true}) + if err != nil { + t.Error("must be no error") + } + resBool, ok := res.Fields["Field2"].(bool) + if !ok { + t.Errorf("Field2 should be bool type, but was %#v\n\nFull resp: %#v", res.Fields["Field2"], res) + } + if !resBool { + t.Errorf("expected that Field_2 will be true, but was: %#v", res.Fields["Field2"].(bool)) + } + record.client.baseURL = mockErrorResponse(404).URL + _, err = record.UpdateRecordPartial(map[string]interface{}{}) + var e *HTTPClientError + if errors.Is(err, e) { + t.Errorf("should be an http error, but was not: %v", err) + } +} + +func testRecord(t *testing.T) *Record { + table := testTable(t) + table.client.baseURL = mockResponse("get_record.json").URL + record, err := table.GetRecord("recordID") + if err != nil { + t.Error("must be no error") + } + return record +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..3ceeec1 --- /dev/null +++ b/table.go @@ -0,0 +1,90 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "net/url" +) + +// Records base type of airtable records +type Records struct { + Records []*Record `json:"records"` + Offset int `offset:"omitempty"` +} + +// Table represents table object +type Table struct { + client *Client + dbName string + tableName string +} + +// GetTable return table object +func (c *Client) GetTable(dbName, tableName string) *Table { + return &Table{ + client: c, + dbName: dbName, + tableName: tableName, + } +} + +// GetRecordsWithParams get records with url values params +// https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:list +func (t *Table) GetRecordsWithParams(params url.Values) (*Records, error) { + records := new(Records) + err := t.client.get(t.dbName, t.tableName, "", params, records) + if err != nil { + return nil, err + } + for _, record := range records.Records { + record.client = t.client + record.table = t + } + return records, nil +} + +// AddRecords method to lines to table (up to 10 in one request) +// https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:create +func (t *Table) AddRecords(records *Records) (*Records, error) { + result := new(Records) + err := t.client.post(t.dbName, t.tableName, records, result) + if err != nil { + return nil, err + } + for _, record := range result.Records { + record.client = t.client + record.table = t + } + return result, err +} + +// UpdateRecords full updates records +func (t *Table) UpdateRecords(records *Records) (*Records, error) { + response := new(Records) + err := t.client.post(t.dbName, t.tableName, records, response) + if err != nil { + return nil, err + } + for _, record := range response.Records { + record.client = t.client + record.table = t + } + return response, nil +} + +// DeleteRecords delete records by recordID +func (t *Table) DeleteRecords(recordIDs []string) (*Records, error) { + response := new(Records) + err := t.client.delete(t.dbName, t.tableName, recordIDs, response) + if err != nil { + return nil, err + } + for _, record := range response.Records { + record.client = t.client + record.table = t + } + return response, nil +} diff --git a/table_test.go b/table_test.go new file mode 100644 index 0000000..ca744ef --- /dev/null +++ b/table_test.go @@ -0,0 +1,74 @@ +// Copyright © 2020 Mike Berezin +// +// Use of this source code is governed by an MIT license. +// Details in the LICENSE file. + +package airtable + +import ( + "errors" + "testing" +) + +func TestTable_DeleteRecords(t *testing.T) { + table := testTable(t) + table.client.baseURL = mockResponse("delete_records.json").URL + records, err := table.DeleteRecords([]string{"recnTq6CsvFM6vX2m", "recr3qAQbM7juKa4o"}) + if err != nil { + t.Error("must be no error") + } + for _, record := range records.Records { + if !record.Deleted { + t.Errorf("expected that record will be deleted, but was: %#v", record.Deleted) + } + } + table.client.baseURL = mockErrorResponse(404).URL + _, err = table.DeleteRecords([]string{}) + var e *HTTPClientError + if errors.Is(err, e) { + t.Errorf("should be an http error, but was not: %v", err) + } +} + +func TestTable_AddRecords(t *testing.T) { + table := testTable(t) + table.client.baseURL = mockResponse("get_records_with_filter.json").URL + toSend := new(Records) + records, err := table.AddRecords(toSend) + if err != nil { + t.Error("must be no error") + } + if len(records.Records) != 3 { + t.Errorf("should be 3 records in result, but was: %v", len(records.Records)) + } + table.client.baseURL = mockErrorResponse(404).URL + _, err = table.AddRecords(toSend) + var e *HTTPClientError + if errors.Is(err, e) { + t.Errorf("should be an http error, but was not: %v", err) + } +} + +func TestTable_UpdateRecords(t *testing.T) { + table := testTable(t) + table.client.baseURL = mockResponse("get_records_with_filter.json").URL + toSend := new(Records) + records, err := table.UpdateRecords(toSend) + if err != nil { + t.Error("must be no error") + } + if len(records.Records) != 3 { + t.Errorf("should be 3 records in result, but was: %v", len(records.Records)) + } + table.client.baseURL = mockErrorResponse(404).URL + _, err = table.UpdateRecords(toSend) + var e *HTTPClientError + if errors.Is(err, e) { + t.Errorf("should be an http error, but was not: %v", err) + } +} + +func testTable(t *testing.T) *Table { + client := testClient(t) + return client.GetTable("dbName", "tableName") +} diff --git a/testdata/create_records.json b/testdata/create_records.json new file mode 100644 index 0000000..1c33526 --- /dev/null +++ b/testdata/create_records.json @@ -0,0 +1,22 @@ +{ + "records": [ + { + "id": "recnTq6CsvFM6vX2m", + "fields": { + "Field1": "Field1", + "Field2": true, + "Field3": "2020-04-06T06:00:00.000Z" + }, + "createdTime": "2020-04-10T11:30:57.000Z" + }, + { + "id": "recr3qAQbM7juKa4o", + "fields": { + "Field1": "Field1", + "Field2": true, + "Field3": "2020-04-06T06:00:00.000Z" + }, + "createdTime": "2020-04-10T11:30:49.000Z" + } + ] +} \ No newline at end of file diff --git a/testdata/delete_record.json b/testdata/delete_record.json new file mode 100644 index 0000000..b79bcb8 --- /dev/null +++ b/testdata/delete_record.json @@ -0,0 +1,8 @@ +{ + "records": [ + { + "id": "recnTq6CsvFM6vX2m", + "deleted": true + } + ] +} \ No newline at end of file diff --git a/testdata/delete_records.json b/testdata/delete_records.json new file mode 100644 index 0000000..92e59bd --- /dev/null +++ b/testdata/delete_records.json @@ -0,0 +1,12 @@ +{ + "records": [ + { + "id": "recnTq6CsvFM6vX2m", + "deleted": true + }, + { + "id": "recr3qAQbM7juKa4o", + "deleted": true + } + ] +} \ No newline at end of file diff --git a/testdata/get_record.json b/testdata/get_record.json new file mode 100644 index 0000000..be22029 --- /dev/null +++ b/testdata/get_record.json @@ -0,0 +1,9 @@ +{ + "id": "recnTq6CsvFM6vX2m", + "fields": { + "Field1": "Field1", + "Field2": true, + "Field3": "2020-04-06T06:00:00.000Z" + }, + "createdTime": "2020-04-10T11:30:57.000Z" +} \ No newline at end of file diff --git a/testdata/get_records_with_filter.json b/testdata/get_records_with_filter.json new file mode 100644 index 0000000..6901191 --- /dev/null +++ b/testdata/get_records_with_filter.json @@ -0,0 +1,31 @@ +{ + "records": [ + { + "id": "recnTq6CsvFM6vX2m", + "fields": { + "Field1": "Field1", + "Field2": true, + "Field3": "2020-04-06T06:00:00.000Z" + }, + "createdTime": "2020-04-10T11:30:57.000Z" + }, + { + "id": "recr3qAQbM7juKa4o", + "fields": { + "Field1": "Field1", + "Field2": true, + "Field3": "2020-04-06T06:00:00.000Z" + }, + "createdTime": "2020-04-10T11:30:49.000Z" + }, + { + "id": "recr3qAQbM7juKa4a", + "fields": { + "Field1": "Field1", + "Field2": false, + "Field3": "2020-04-06T06:00:00.000Z" + }, + "createdTime": "2020-04-10T11:30:49.000Z" + } + ] +} \ No newline at end of file