feat: add and get records methods (initial commit)

This commit is contained in:
mehanizm
2020-04-12 13:05:28 +03:00
commit 2a2e70bfe7
21 changed files with 870 additions and 0 deletions

65
.gitignore vendored Normal file
View 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
View 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
View File

@@ -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)
<a href='https://github.com/jpoles1/gopherbadger' target='_blank'>![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-92%25-brightgreen.svg?longCache=true&style=flat)</a>
<a href='https://goreportcard.com/report/github.com/mehanizm/airtable' target='_blank'>![goreportcard](https://goreportcard.com/badge/github.com/mehanizm/airtable)</a>
A simple #golang package to access the [Airtable API](https://airtable.com/api).

145
client.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,8 @@
{
"records": [
{
"id": "recnTq6CsvFM6vX2m",
"deleted": true
}
]
}

12
testdata/delete_records.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"records": [
{
"id": "recnTq6CsvFM6vX2m",
"deleted": true
},
{
"id": "recr3qAQbM7juKa4o",
"deleted": true
}
]
}

9
testdata/get_record.json vendored Normal file
View 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
View 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"
}
]
}