Feature/put post (#9)

This commit is contained in:
Yaroslav Rudenko
2021-10-02 14:27:00 +03:00
committed by GitHub
parent 8deb157f4d
commit 3bd1ace8d8
6 changed files with 107 additions and 28 deletions

View File

@@ -7,6 +7,7 @@ package airtable
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -21,7 +22,7 @@ const (
rateLimit = 4 rateLimit = 4
) )
// Client client for airtable api // Client client for airtable api.
type Client struct { type Client struct {
client *http.Client client *http.Client
rateLimiter <-chan time.Time rateLimiter <-chan time.Time
@@ -73,74 +74,118 @@ func (at *Client) rateLimit() {
func (at *Client) get(db, table, recordID string, params url.Values, target interface{}) error { func (at *Client) get(db, table, recordID string, params url.Values, target interface{}) error {
at.rateLimit() at.rateLimit()
url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
if recordID != "" { if recordID != "" {
url += fmt.Sprintf("/%s", recordID) url += fmt.Sprintf("/%s", recordID)
} }
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
if err != nil { if err != nil {
return fmt.Errorf("cannot create request: %w", err) return fmt.Errorf("cannot create request: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
req.URL.RawQuery = params.Encode() req.URL.RawQuery = params.Encode()
err = at.do(req, target) err = at.do(req, target)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (at *Client) post(db, table string, data, response interface{}) error { func (at *Client) post(db, table string, data, response interface{}) error {
at.rateLimit() at.rateLimit()
url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
body, err := json.Marshal(data) body, err := json.Marshal(data)
if err != nil { if err != nil {
return fmt.Errorf("cannot marshal body: %w", err) return fmt.Errorf("cannot marshal body: %w", err)
} }
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewReader(body))
if err != nil { if err != nil {
return fmt.Errorf("cannot create request: %w", err) return fmt.Errorf("cannot create request: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
return at.do(req, response) return at.do(req, response)
} }
func (at *Client) delete(db, table string, recordIDs []string, target interface{}) error { func (at *Client) delete(db, table string, recordIDs []string, target interface{}) error {
at.rateLimit() at.rateLimit()
rawURL := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) rawURL := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
params := url.Values{} params := url.Values{}
for _, recordID := range recordIDs { for _, recordID := range recordIDs {
params.Add("records[]", recordID) params.Add("records[]", recordID)
} }
req, err := http.NewRequest("DELETE", rawURL, nil)
req, err := http.NewRequestWithContext(context.Background(), "DELETE", rawURL, nil)
if err != nil { if err != nil {
return fmt.Errorf("cannot create request: %w", err) return fmt.Errorf("cannot create request: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
req.URL.RawQuery = params.Encode() req.URL.RawQuery = params.Encode()
err = at.do(req, target) err = at.do(req, target)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (at *Client) patch(db, table, data, response interface{}) error { func (at *Client) patch(db, table, data, response interface{}) error {
at.rateLimit() at.rateLimit()
url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table) url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
body, err := json.Marshal(data) body, err := json.Marshal(data)
if err != nil { if err != nil {
return fmt.Errorf("cannot marshal body: %w", err) return fmt.Errorf("cannot marshal body: %w", err)
} }
req, err := http.NewRequest("PATCH", url, bytes.NewReader(body))
req, err := http.NewRequestWithContext(context.Background(), "PATCH", url, bytes.NewReader(body))
if err != nil { if err != nil {
return fmt.Errorf("cannot create request: %w", err) return fmt.Errorf("cannot create request: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
return at.do(req, response)
}
func (at *Client) put(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.NewRequestWithContext(context.Background(), "PUT", 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, response) return at.do(req, response)
} }
@@ -148,22 +193,29 @@ func (at *Client) do(req *http.Request, response interface{}) error {
if req == nil { if req == nil {
return errors.New("nil request") return errors.New("nil request")
} }
url := req.URL.RequestURI() url := req.URL.RequestURI()
resp, err := at.client.Do(req) resp, err := at.client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("HTTP request failure on %s: %w", url, err) return fmt.Errorf("HTTP request failure on %s: %w", url, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 { if resp.StatusCode < 200 || resp.StatusCode > 299 {
return makeHTTPClientError(url, resp) return makeHTTPClientError(url, resp)
} }
b, err := ioutil.ReadAll(resp.Body) b, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("HTTP Read error on response for %s: %w", url, err) return fmt.Errorf("HTTP Read error on response for %s: %w", url, err)
} }
err = json.Unmarshal(b, response) err = json.Unmarshal(b, response)
if err != nil { if err != nil {
return fmt.Errorf("JSON decode failed on %s:\n%s\nerror: %w", url, string(b), err) return fmt.Errorf("JSON decode failed on %s:\n%s\nerror: %w", url, string(b), err)
} }
return nil return nil
} }

View File

@@ -40,5 +40,4 @@ func main() {
break break
} }
} }
} }

View File

@@ -11,7 +11,7 @@ import (
"net/http" "net/http"
) )
// HTTPClientError custom error to handle with response status // HTTPClientError custom error to handle with response status.
type HTTPClientError struct { type HTTPClientError struct {
StatusCode int StatusCode int
Err error Err error
@@ -23,12 +23,14 @@ func (e *HTTPClientError) Error() string {
func makeHTTPClientError(url string, resp *http.Response) error { func makeHTTPClientError(url string, resp *http.Response) error {
var resError error var resError error
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
resError = fmt.Errorf("HTTP request failure on %s with status %d\nCannot parse body with: %w", url, resp.StatusCode, err) resError = fmt.Errorf("HTTP request failure on %s with status %d\nCannot parse body with: %w", url, resp.StatusCode, err)
} else { } else {
resError = fmt.Errorf("HTTP request failure on %s with status %d\nBody: %v", url, resp.StatusCode, string(body)) resError = fmt.Errorf("HTTP request failure on %s with status %d\nBody: %v", url, resp.StatusCode, string(body))
} }
return &HTTPClientError{ return &HTTPClientError{
StatusCode: resp.StatusCode, StatusCode: resp.StatusCode,
Err: resError, Err: resError,

View File

@@ -11,14 +11,14 @@ import (
"strconv" "strconv"
) )
// GetRecordsConfig helper type to use in // GetRecordsConfig helper type to use in.
// step by step get records // step by step get records.
type GetRecordsConfig struct { type GetRecordsConfig struct {
table *Table table *Table
params url.Values params url.Values
} }
// GetRecords prepare step to get records // GetRecords prepare step to get records.
func (t *Table) GetRecords() *GetRecordsConfig { func (t *Table) GetRecords() *GetRecordsConfig {
return &GetRecordsConfig{ return &GetRecordsConfig{
table: t, table: t,
@@ -26,7 +26,7 @@ func (t *Table) GetRecords() *GetRecordsConfig {
} }
} }
// ReturnFields set returning field names // ReturnFields set returning field names.
func (grc *GetRecordsConfig) ReturnFields(fieldNames ...string) *GetRecordsConfig { func (grc *GetRecordsConfig) ReturnFields(fieldNames ...string) *GetRecordsConfig {
for _, fieldName := range fieldNames { for _, fieldName := range fieldNames {
grc.params.Add("fields[]", fieldName) grc.params.Add("fields[]", fieldName)
@@ -34,13 +34,13 @@ func (grc *GetRecordsConfig) ReturnFields(fieldNames ...string) *GetRecordsConfi
return grc return grc
} }
// WithFilterFormula add filter to request // WithFilterFormula add filter to request.
func (grc *GetRecordsConfig) WithFilterFormula(filterFormula string) *GetRecordsConfig { func (grc *GetRecordsConfig) WithFilterFormula(filterFormula string) *GetRecordsConfig {
grc.params.Set("filterByFormula", filterFormula) grc.params.Set("filterByFormula", filterFormula)
return grc return grc
} }
// WithSort add sorting to request // WithSort add sorting to request.
func (grc *GetRecordsConfig) WithSort(sortQueries ...struct { func (grc *GetRecordsConfig) WithSort(sortQueries ...struct {
fieldName string fieldName string
direction string direction string
@@ -52,13 +52,13 @@ func (grc *GetRecordsConfig) WithSort(sortQueries ...struct {
return grc return grc
} }
// FromView add view parameter to get records // FromView add view parameter to get records.
func (grc *GetRecordsConfig) FromView(viewNameOrID string) *GetRecordsConfig { func (grc *GetRecordsConfig) FromView(viewNameOrID string) *GetRecordsConfig {
grc.params.Set("view", viewNameOrID) grc.params.Set("view", viewNameOrID)
return grc return grc
} }
// The maximum total number of records that will be returned in your requests. // MaxRecords The maximum total number of records that will be returned in your requests.
// If this value is larger than pageSize (which is 100 by default), // If this value is larger than pageSize (which is 100 by default),
// you may have to load multiple pages to reach this total. // you may have to load multiple pages to reach this total.
// See the Pagination section below for more. // See the Pagination section below for more.
@@ -67,7 +67,7 @@ func (grc *GetRecordsConfig) MaxRecords(maxRecords int) *GetRecordsConfig {
return grc return grc
} }
// The number of records returned in each request. // PageSize The number of records returned in each request.
// Must be less than or equal to 100. Default is 100. // Must be less than or equal to 100. Default is 100.
// See the Pagination section below for more. // See the Pagination section below for more.
func (grc *GetRecordsConfig) PageSize(pageSize int) *GetRecordsConfig { func (grc *GetRecordsConfig) PageSize(pageSize int) *GetRecordsConfig {
@@ -82,14 +82,14 @@ func (grc *GetRecordsConfig) PageSize(pageSize int) *GetRecordsConfig {
// If there are more records, the response will contain an offset. // If there are more records, the response will contain an offset.
// To fetch the next page of records, include offset in the next request's parameters. // To fetch the next page of records, include offset in the next request's parameters.
// Pagination will stop when you've reached the end of your table. // WithOffset Pagination will stop when you've reached the end of your table.
// If the maxRecords parameter is passed, pagination will stop once you've reached this maximum. // If the maxRecords parameter is passed, pagination will stop once you've reached this maximum.
func (grc *GetRecordsConfig) WithOffset(offset string) *GetRecordsConfig { func (grc *GetRecordsConfig) WithOffset(offset string) *GetRecordsConfig {
grc.params.Set("offset", offset) grc.params.Set("offset", offset)
return grc return grc
} }
// InStringFormat add parameter to get records in string format // InStringFormat add parameter to get records in string format.
// it require timezone // it require timezone
// https://support.airtable.com/hc/en-us/articles/216141558-Supported-timezones-for-SET-TIMEZONE // https://support.airtable.com/hc/en-us/articles/216141558-Supported-timezones-for-SET-TIMEZONE
// and user locale data // and user locale data
@@ -101,7 +101,7 @@ func (grc *GetRecordsConfig) InStringFormat(timeZone, userLocale string) *GetRec
return grc return grc
} }
// Do send the prepared get records request // Do send the prepared get records request.
func (grc *GetRecordsConfig) Do() (*Records, error) { func (grc *GetRecordsConfig) Do() (*Records, error) {
return grc.table.GetRecordsWithParams(grc.params) return grc.table.GetRecordsWithParams(grc.params)
} }

View File

@@ -7,7 +7,7 @@ package airtable
import "net/url" import "net/url"
// Record base time of airtable record fields // Record base time of airtable record fields.
type Record struct { type Record struct {
client *Client client *Client
table *Table table *Table
@@ -27,16 +27,19 @@ type Record struct {
// https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:retrieve // https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:retrieve
func (t *Table) GetRecord(recordID string) (*Record, error) { func (t *Table) GetRecord(recordID string) (*Record, error) {
result := new(Record) result := new(Record)
err := t.client.get(t.dbName, t.tableName, recordID, url.Values{}, result) err := t.client.get(t.dbName, t.tableName, recordID, url.Values{}, result)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result.client = t.client result.client = t.client
result.table = t result.table = t
return result, nil return result, nil
} }
// UpdateRecordPartial updates partial info on record // UpdateRecordPartial updates partial info on record.
func (r *Record) UpdateRecordPartial(changedFields map[string]interface{}) (*Record, error) { func (r *Record) UpdateRecordPartial(changedFields map[string]interface{}) (*Record, error) {
data := &Records{ data := &Records{
Records: []*Record{ Records: []*Record{
@@ -47,25 +50,32 @@ func (r *Record) UpdateRecordPartial(changedFields map[string]interface{}) (*Rec
}, },
} }
response := new(Records) response := new(Records)
err := r.client.patch(r.table.dbName, r.table.tableName, data, response) err := r.client.patch(r.table.dbName, r.table.tableName, data, response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := response.Records[0] result := response.Records[0]
result.client = r.client result.client = r.client
result.table = r.table result.table = r.table
return result, nil return result, nil
} }
// DeleteRecord delete one record // DeleteRecord delete one record.
func (r *Record) DeleteRecord() (*Record, error) { func (r *Record) DeleteRecord() (*Record, error) {
response := new(Records) response := new(Records)
err := r.client.delete(r.table.dbName, r.table.tableName, []string{r.ID}, response) err := r.client.delete(r.table.dbName, r.table.tableName, []string{r.ID}, response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := response.Records[0] result := response.Records[0]
result.client = r.client result.client = r.client
result.table = r.table result.table = r.table
return result, nil return result, nil
} }

View File

@@ -9,7 +9,7 @@ import (
"net/url" "net/url"
) )
// Records base type of airtable records // Records base type of airtable records.
type Records struct { type Records struct {
Records []*Record `json:"records"` Records []*Record `json:"records"`
Offset string `json:"offset,omitempty"` Offset string `json:"offset,omitempty"`
@@ -21,14 +21,14 @@ type Records struct {
Typecast bool `json:"typecast,omitempty"` Typecast bool `json:"typecast,omitempty"`
} }
// Table represents table object // Table represents table object.
type Table struct { type Table struct {
client *Client client *Client
dbName string dbName string
tableName string tableName string
} }
// GetTable return table object // GetTable return table object.
func (c *Client) GetTable(dbName, tableName string) *Table { func (c *Client) GetTable(dbName, tableName string) *Table {
return &Table{ return &Table{
client: c, client: c,
@@ -41,14 +41,17 @@ func (c *Client) GetTable(dbName, tableName string) *Table {
// https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:list // https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:list
func (t *Table) GetRecordsWithParams(params url.Values) (*Records, error) { func (t *Table) GetRecordsWithParams(params url.Values) (*Records, error) {
records := new(Records) records := new(Records)
err := t.client.get(t.dbName, t.tableName, "", params, records) err := t.client.get(t.dbName, t.tableName, "", params, records)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, record := range records.Records { for _, record := range records.Records {
record.client = t.client record.client = t.client
record.table = t record.table = t
} }
return records, nil return records, nil
} }
@@ -56,55 +59,68 @@ func (t *Table) GetRecordsWithParams(params url.Values) (*Records, error) {
// https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:create // https://airtable.com/{yourDatabaseID}/api/docs#curl/table:{yourTableName}:create
func (t *Table) AddRecords(records *Records) (*Records, error) { func (t *Table) AddRecords(records *Records) (*Records, error) {
result := new(Records) result := new(Records)
err := t.client.post(t.dbName, t.tableName, records, result) err := t.client.post(t.dbName, t.tableName, records, result)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, record := range result.Records { for _, record := range result.Records {
record.client = t.client record.client = t.client
record.table = t record.table = t
} }
return result, err return result, err
} }
// UpdateRecords full update records // UpdateRecords full update records.
func (t *Table) UpdateRecords(records *Records) (*Records, error) { func (t *Table) UpdateRecords(records *Records) (*Records, error) {
response := new(Records) response := new(Records)
err := t.client.post(t.dbName, t.tableName, records, response)
err := t.client.put(t.dbName, t.tableName, records, response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, record := range response.Records { for _, record := range response.Records {
record.client = t.client record.client = t.client
record.table = t record.table = t
} }
return response, nil return response, nil
} }
// UpdateRecordsPartial partial update records // UpdateRecordsPartial partial update records.
func (t *Table) UpdateRecordsPartial(records *Records) (*Records, error) { func (t *Table) UpdateRecordsPartial(records *Records) (*Records, error) {
response := new(Records) response := new(Records)
err := t.client.patch(t.dbName, t.tableName, records, response) err := t.client.patch(t.dbName, t.tableName, records, response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, record := range response.Records { for _, record := range response.Records {
record.client = t.client record.client = t.client
record.table = t record.table = t
} }
return response, nil return response, nil
} }
// DeleteRecords delete records by recordID // DeleteRecords delete records by recordID
// up to 10 ids in one request.
func (t *Table) DeleteRecords(recordIDs []string) (*Records, error) { func (t *Table) DeleteRecords(recordIDs []string) (*Records, error) {
response := new(Records) response := new(Records)
err := t.client.delete(t.dbName, t.tableName, recordIDs, response) err := t.client.delete(t.dbName, t.tableName, recordIDs, response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, record := range response.Records { for _, record := range response.Records {
record.client = t.client record.client = t.client
record.table = t record.table = t
} }
return response, nil return response, nil
} }