From 3bd1ace8d8ae5521c8fe53e3b90ca16a7bd2e046 Mon Sep 17 00:00:00 2001 From: Yaroslav Rudenko Date: Sat, 2 Oct 2021 14:27:00 +0300 Subject: [PATCH] Feature/put post (#9) --- client.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++---- cmd/main.go | 1 - errors.go | 4 +++- get-records.go | 24 +++++++++---------- record.go | 16 ++++++++++--- table.go | 28 ++++++++++++++++++----- 6 files changed, 107 insertions(+), 28 deletions(-) diff --git a/client.go b/client.go index d136811..c5174d0 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ package airtable import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -21,7 +22,7 @@ const ( rateLimit = 4 ) -// Client client for airtable api +// Client client for airtable api. type Client struct { client *http.Client 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 { 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) + + req, err := http.NewRequestWithContext(context.Background(), "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, 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)) + + req, err := http.NewRequestWithContext(context.Background(), "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, 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.Add("records[]", recordID) } - req, err := http.NewRequest("DELETE", rawURL, nil) + + req, err := http.NewRequestWithContext(context.Background(), "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, 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)) + + req, err := http.NewRequestWithContext(context.Background(), "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, 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) } @@ -148,22 +193,29 @@ func (at *Client) do(req *http.Request, response interface{}) error { if req == nil { return errors.New("nil request") } + url := req.URL.RequestURI() + 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/cmd/main.go b/cmd/main.go index d971582..cbd9a85 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -40,5 +40,4 @@ func main() { break } } - } diff --git a/errors.go b/errors.go index 1d19871..278bf12 100644 --- a/errors.go +++ b/errors.go @@ -11,7 +11,7 @@ import ( "net/http" ) -// HTTPClientError custom error to handle with response status +// HTTPClientError custom error to handle with response status. type HTTPClientError struct { StatusCode int Err error @@ -23,12 +23,14 @@ func (e *HTTPClientError) Error() string { 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 index 63754b0..075486e 100644 --- a/get-records.go +++ b/get-records.go @@ -11,14 +11,14 @@ import ( "strconv" ) -// GetRecordsConfig helper type to use in -// step by step get records +// 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 +// GetRecords prepare step to get records. func (t *Table) GetRecords() *GetRecordsConfig { return &GetRecordsConfig{ 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 { for _, fieldName := range fieldNames { grc.params.Add("fields[]", fieldName) @@ -34,13 +34,13 @@ func (grc *GetRecordsConfig) ReturnFields(fieldNames ...string) *GetRecordsConfi return grc } -// WithFilterFormula add filter to request +// WithFilterFormula add filter to request. func (grc *GetRecordsConfig) WithFilterFormula(filterFormula string) *GetRecordsConfig { grc.params.Set("filterByFormula", filterFormula) return grc } -// WithSort add sorting to request +// WithSort add sorting to request. func (grc *GetRecordsConfig) WithSort(sortQueries ...struct { fieldName string direction string @@ -52,13 +52,13 @@ func (grc *GetRecordsConfig) WithSort(sortQueries ...struct { return grc } -// FromView add view parameter to get records +// FromView add view parameter to get records. func (grc *GetRecordsConfig) FromView(viewNameOrID string) *GetRecordsConfig { grc.params.Set("view", viewNameOrID) 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), // you may have to load multiple pages to reach this total. // See the Pagination section below for more. @@ -67,7 +67,7 @@ func (grc *GetRecordsConfig) MaxRecords(maxRecords int) *GetRecordsConfig { 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. // See the Pagination section below for more. 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. // 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. func (grc *GetRecordsConfig) WithOffset(offset string) *GetRecordsConfig { grc.params.Set("offset", offset) return grc } -// InStringFormat add parameter to get records in string format +// 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 @@ -101,7 +101,7 @@ func (grc *GetRecordsConfig) InStringFormat(timeZone, userLocale string) *GetRec return grc } -// Do send the prepared get records request +// Do send the prepared get records request. func (grc *GetRecordsConfig) Do() (*Records, error) { return grc.table.GetRecordsWithParams(grc.params) } diff --git a/record.go b/record.go index 1419829..3927986 100644 --- a/record.go +++ b/record.go @@ -7,7 +7,7 @@ package airtable import "net/url" -// Record base time of airtable record fields +// Record base time of airtable record fields. type Record struct { client *Client table *Table @@ -27,16 +27,19 @@ type Record struct { // 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 +// UpdateRecordPartial updates partial info on record. func (r *Record) UpdateRecordPartial(changedFields map[string]interface{}) (*Record, error) { data := &Records{ Records: []*Record{ @@ -47,25 +50,32 @@ func (r *Record) UpdateRecordPartial(changedFields map[string]interface{}) (*Rec }, } 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 +// 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/table.go b/table.go index 000b0bb..199ecf4 100644 --- a/table.go +++ b/table.go @@ -9,7 +9,7 @@ import ( "net/url" ) -// Records base type of airtable records +// Records base type of airtable records. type Records struct { Records []*Record `json:"records"` Offset string `json:"offset,omitempty"` @@ -21,14 +21,14 @@ type Records struct { Typecast bool `json:"typecast,omitempty"` } -// Table represents table object +// Table represents table object. type Table struct { client *Client dbName string tableName string } -// GetTable return table object +// GetTable return table object. func (c *Client) GetTable(dbName, tableName string) *Table { return &Table{ client: c, @@ -41,14 +41,17 @@ func (c *Client) GetTable(dbName, tableName string) *Table { // 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 } @@ -56,55 +59,68 @@ func (t *Table) GetRecordsWithParams(params url.Values) (*Records, error) { // 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 update records +// UpdateRecords full update records. func (t *Table) UpdateRecords(records *Records) (*Records, error) { 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 { return nil, err } + for _, record := range response.Records { record.client = t.client record.table = t } + return response, nil } -// UpdateRecordsPartial partial update records +// UpdateRecordsPartial partial update records. func (t *Table) UpdateRecordsPartial(records *Records) (*Records, error) { response := new(Records) + err := t.client.patch(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 +// up to 10 ids in one request. 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 }