From 1b1e6e9e92d64232c417db54165aeaf549b0ef40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20de=20Metz?= Date: Sun, 1 Jan 2023 15:18:29 +0100 Subject: [PATCH] feat: add read-only support of the meta API (#13) * Add bases API. * Implement get tables schema * Rename GetBase to GetBaseSchema * Add tests * Update doc --- README.md | 12 ++++++ base.go | 89 +++++++++++++++++++++++++++++++++++++++ base_test.go | 25 +++++++++++ get-bases.go | 37 ++++++++++++++++ get-bases_test.go | 25 +++++++++++ testdata/base_schema.json | 72 +++++++++++++++++++++++++++++++ testdata/get_bases.json | 15 +++++++ 7 files changed, 275 insertions(+) create mode 100644 base.go create mode 100644 base_test.go create mode 100644 get-bases.go create mode 100644 get-bases_test.go create mode 100644 testdata/base_schema.json create mode 100644 testdata/get_bases.json diff --git a/README.md b/README.md index c3ee26c..c6386bf 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,18 @@ You should get `your_api_token` in the airtable [account page](https://airtable. client := airtable.NewClient("your_api_token") ``` +### List bases + +```Go +bases, err := client.GetBases().WithOffset("").Do() +``` + +### Get base schema + +```Go +schema, err := client.GetBaseSchema("your_database_ID").Do() +``` + ### Get table To get the `your_database_ID` you should go to [main API page](https://airtable.com/api) and select the database. diff --git a/base.go b/base.go new file mode 100644 index 0000000..6f99ab4 --- /dev/null +++ b/base.go @@ -0,0 +1,89 @@ +package airtable + +import ( + "net/url" +) + +// Base type of airtable base. +type Base struct { + ID string `json:"id"` + Name string `json:"name"` + PermissionLevel string `json:"permissionLevel"` +} + +// Base type of airtable bases. +type Bases struct { + Bases []*Base `json:"bases"` + Offset string `json:"offset,omitempty"` +} + +type Field struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` +} +type View struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` +} + +type TableSchema struct { + ID string `json:"id"` + PrimaryFieldID string `json:"primaryFieldId"` + Name string `json:"name"` + Description string `json:"description"` + Fields []*Field `json:"fields"` + Views []*View `json:"views"` +} + +type Tables struct { + Tables []*TableSchema `json:"tables"` +} + +// GetBasesWithParams get bases with url values params +// https://airtable.com/developers/web/api/list-bases +func (at *Client) GetBasesWithParams(params url.Values) (*Bases, error) { + bases := new(Bases) + + err := at.get("meta", "bases", "", params, bases) + if err != nil { + return nil, err + } + + return bases, nil +} + +// Table represents table object. +type BaseConfig struct { + client *Client + dbId string + params url.Values +} + +// GetBase return Base object. +func (c *Client) GetBaseSchema(dbId string) *BaseConfig { + return &BaseConfig{ + client: c, + dbId: dbId, + } +} + +// Do send the prepared +func (b *BaseConfig) Do() (*Tables, error) { + return b.GetTablesWithParams() +} + +// GetTablesWithParams get tables from a base with url values params +// https://airtable.com/developers/web/api/get-base-schema +func (b *BaseConfig) GetTablesWithParams() (*Tables, error) { + tables := new(Tables) + + err := b.client.get("meta/bases", b.dbId, "tables", nil, tables) + if err != nil { + return nil, err + } + + return tables, nil +} diff --git a/base_test.go b/base_test.go new file mode 100644 index 0000000..8b5d828 --- /dev/null +++ b/base_test.go @@ -0,0 +1,25 @@ +package airtable + +import ( + "testing" +) + +func TestGetBaseSchema(t *testing.T) { + client := testClient(t) + baseschema := client.GetBaseSchema("test") + baseschema.client.baseURL = mockResponse("base_schema.json").URL + + result, err := baseschema.Do() + if err != nil { + t.Errorf("there should not be an err, but was: %v", err) + } + if len(result.Tables) != 2 { + t.Errorf("there should be 2 tales, but was %v", len(result.Tables)) + } + + baseschema.client.baseURL = mockErrorResponse(400).URL + _, err = baseschema.Do() + if err == nil { + t.Errorf("there should be an err, but was nil") + } +} diff --git a/get-bases.go b/get-bases.go new file mode 100644 index 0000000..3050950 --- /dev/null +++ b/get-bases.go @@ -0,0 +1,37 @@ +package airtable + +import ( + "net/url" +) + +// GetBasesConfig helper type to use in. +// step by step get bases. +type GetBasesConfig struct { + client *Client + params url.Values +} + +// GetBases prepare step to get bases. +func (c *Client) GetBases() *GetBasesConfig { + return &GetBasesConfig{ + client: c, + params: url.Values{}, + } +} + +// Pagination +// The server returns one page of bases at a time. + +// 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. + +// WithOffset Pagination will stop when you've reached the end of your bases. +func (gbc *GetBasesConfig) WithOffset(offset string) *GetBasesConfig { + gbc.params.Set("offset", offset) + return gbc +} + +// Do send the prepared get records request. +func (gbc *GetBasesConfig) Do() (*Bases, error) { + return gbc.client.GetBasesWithParams(gbc.params) +} diff --git a/get-bases_test.go b/get-bases_test.go new file mode 100644 index 0000000..ec62f8d --- /dev/null +++ b/get-bases_test.go @@ -0,0 +1,25 @@ +package airtable + +import ( + "testing" +) + +func TestGetBases_Do(t *testing.T) { + client := testClient(t) + bases := client.GetBases() + bases.client.baseURL = mockResponse("get_bases.json").URL + + result, err := bases.WithOffset("0").Do() + if err != nil { + t.Errorf("there should not be an err, but was: %v", err) + } + if len(result.Bases) != 2 { + t.Errorf("there should be 2 bases, but was %v", len(result.Bases)) + } + + bases.client.baseURL = mockErrorResponse(400).URL + _, err = bases.Do() + if err == nil { + t.Errorf("there should be an err, but was nil") + } +} diff --git a/testdata/base_schema.json b/testdata/base_schema.json new file mode 100644 index 0000000..993cae6 --- /dev/null +++ b/testdata/base_schema.json @@ -0,0 +1,72 @@ +{ + "tables": [ + { + "description": "Apartments to track.", + "fields": [ + { + "description": "Name of the apartment", + "id": "fld1VnoyuotSTyxW1", + "name": "Name", + "type": "singleLineText" + }, + { + "id": "fldoaIqdn5szURHpw", + "name": "Pictures", + "type": "multipleAttachments" + }, + { + "id": "fldumZe00w09RYTW6", + "name": "District", + "options": { + "inverseLinkFieldId": "fldWnCJlo2z6ttT8Y", + "isReversed": false, + "linkedTableId": "tblK6MZHez0ZvBChZ", + "prefersSingleRecordLink": true + }, + "type": "multipleRecordLinks" + } + ], + "id": "tbltp8DGLhqbUmjK1", + "name": "Apartments", + "primaryFieldId": "fld1VnoyuotSTyxW1", + "views": [ + { + "id": "viwQpsuEDqHFqegkp", + "name": "Grid view", + "type": "grid" + } + ] + }, + { + "fields": [ + { + "id": "fldEVzvQOoULO38yl", + "name": "Name", + "type": "singleLineText" + }, + { + "description": "Apartments that belong to this district", + "id": "fldWnCJlo2z6ttT8Y", + "name": "Apartments", + "options": { + "inverseLinkFieldId": "fldumZe00w09RYTW6", + "isReversed": false, + "linkedTableId": "tbltp8DGLhqbUmjK1", + "prefersSingleRecordLink": false + }, + "type": "multipleRecordLinks" + } + ], + "id": "tblK6MZHez0ZvBChZ", + "name": "Districts", + "primaryFieldId": "fldEVzvQOoULO38yl", + "views": [ + { + "id": "viwi3KXvrKug2mIBS", + "name": "Grid view", + "type": "grid" + } + ] + } + ] +} diff --git a/testdata/get_bases.json b/testdata/get_bases.json new file mode 100644 index 0000000..c59793b --- /dev/null +++ b/testdata/get_bases.json @@ -0,0 +1,15 @@ +{ + "bases": [ + { + "id": "appLkNDICXNqxSDhG", + "name": "Apartment Hunting", + "permissionLevel": "create" + }, + { + "id": "appSW9R5uCNmRmfl6", + "name": "Project Tracker", + "permissionLevel": "edit" + } + ], + "offset": "itr23sEjsdfEr3282/appSW9R5uCNmRmfl6" +}