feat: add and get records methods (initial commit)
This commit is contained in:
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal file
@@ -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
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||
8
README.md
Normal file
8
README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Golang Airtable API
|
||||
================
|
||||
|
||||
[](https://pkg.go.dev/github.com/mehanizm/airtable)
|
||||
<a href='https://github.com/jpoles1/gopherbadger' target='_blank'></a>
|
||||
<a href='https://goreportcard.com/report/github.com/mehanizm/airtable' target='_blank'></a>
|
||||
|
||||
A simple #golang package to access the [Airtable API](https://airtable.com/api).
|
||||
145
client.go
Normal file
145
client.go
Normal file
@@ -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
|
||||
}
|
||||
33
client_test.go
Normal file
33
client_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
7
coverage_badge.png
Normal file
7
coverage_badge.png
Normal file
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<head><title>522 Origin Connection Time-out</title></head>
|
||||
<body bgcolor="white">
|
||||
<center><h1>522 Origin Connection Time-out</h1></center>
|
||||
<hr><center>cloudflare-nginx</center>
|
||||
</body>
|
||||
</html>
|
||||
36
errors.go
Normal file
36
errors.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
75
get-records.go
Normal file
75
get-records.go
Normal file
@@ -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)
|
||||
}
|
||||
37
get-records_test.go
Normal file
37
get-records_test.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/mehanizm/airtable
|
||||
|
||||
go 1.13
|
||||
|
||||
require gopkg.in/square/go-jose.v2 v2.5.0 // indirect
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -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=
|
||||
34
mock-response_test.go
Normal file
34
mock-response_test.go
Normal file
@@ -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)
|
||||
}))
|
||||
}
|
||||
65
record.go
Normal file
65
record.go
Normal file
@@ -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
|
||||
}
|
||||
91
record_test.go
Normal file
91
record_test.go
Normal file
@@ -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
|
||||
}
|
||||
90
table.go
Normal file
90
table.go
Normal file
@@ -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
|
||||
}
|
||||
74
table_test.go
Normal file
74
table_test.go
Normal file
@@ -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")
|
||||
}
|
||||
22
testdata/create_records.json
vendored
Normal file
22
testdata/create_records.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
testdata/delete_record.json
vendored
Normal file
8
testdata/delete_record.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"id": "recnTq6CsvFM6vX2m",
|
||||
"deleted": true
|
||||
}
|
||||
]
|
||||
}
|
||||
12
testdata/delete_records.json
vendored
Normal file
12
testdata/delete_records.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"id": "recnTq6CsvFM6vX2m",
|
||||
"deleted": true
|
||||
},
|
||||
{
|
||||
"id": "recr3qAQbM7juKa4o",
|
||||
"deleted": true
|
||||
}
|
||||
]
|
||||
}
|
||||
9
testdata/get_record.json
vendored
Normal file
9
testdata/get_record.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
31
testdata/get_records_with_filter.json
vendored
Normal file
31
testdata/get_records_with_filter.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user