Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New release #27

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
AllCops:
Exclude:
- 'spec/**/*'
TargetRubyVersion: 2.1
TargetRubyVersion: 2.1
Metrics/LineLength:
Max: 100
Metrics/ClassLength:
Exclude:
- "lib/airtable/entity/table.rb"
Metrics/AbcSize:
Max: 16
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,20 @@ First, be sure to register for an [airtable](https://airtable.com) account, crea
```ruby
# Pass in api key to client
@client = Airtable::Client.new("keyPCx5W")
# Or if you have AIRTABLE_KEY varibale you can use
```

Also you can have `AIRTABLE_KEY` environment variable which is your API key.

```ruby
# if you have AIRTABLE_KEY variable
@client = Airtable::Client.new
```

Your API key carries the same privileges as your user account, so be sure to keep it secret!

### Accessing a Base

Now we can access any base in our Airsheet account by referencing the [API docs](https://airtable.com/api):
Now we can access any base in our Airtable account by referencing the [API docs](https://airtable.com/api):

```ruby
# Pass in the base id
Expand All @@ -49,7 +55,7 @@ Now we can access any base in our Airsheet account by referencing the [API docs]

### Accessing a Table

Now we can access any table in our Airsheet account by referencing the [API docs](https://airtable.com/api):
Now we can access any table in our Airtable account by referencing the [API docs](https://airtable.com/api):

```ruby
# Pass in the table name
Expand Down Expand Up @@ -123,11 +129,11 @@ Records can be destroyed using the `destroy` method on a table:

## Command Line Tool

This gem is include a very simple command line tool which can show basic functionality of service.
This gem includes a simple command line tool which shows the basic functionality of the service.

```
$ airtable
Usage: airtable [options]
Usage: airtable operation options

Common options:
-k, --api_key=KEY Airtable API key
Expand All @@ -138,9 +144,15 @@ Common options:
-v, --value VALUE Field value for update

Supported Operations:
Get Record (if only RECORD_ID provided)
Get Field (if RECORD_ID and FIELD_ID are provided)
Update Field (if RECORD_ID, FIELD_ID and VALUE are provided)
get - Get Record/Field
update - Update Field

Examples:
airtable get -B Base -t Table
airtable get -B Base -t Table -r RECORD_ID
airtable get -B Base -t Table -f FIELD_NAME
airtable get -B Base -t Table -f FIELD_NAME -r RECORD_ID
airtable update -b Base -t table -r RECORD_ID -f FIELD_NAME -v newValue

-h, --help Show this message
--version Show version
Expand All @@ -149,21 +161,21 @@ Supported Operations:
### Get record's JSON

```
$ airtable -b base_id -t Table -r record_id
$ airtable get -b base_id -t Table -r record_id
{"id":"record_id","fields":{...},"createdTime":"2015-11-11 23:05:58 UTC"}
```

### Get record's field value

```
$ airtable -b base_id -t Table -r record_id -f field_name
$ airtable get -b base_id -t Table -r record_id -f field_name
FIELD_VALUE
```

### Update record's field value

```
$ airtable -b base_id -t Table -r record_id -f field_name -v NEW_VALUE
$ airtable update -b base_id -t Table -r record_id -f field_name -v NEW_VALUE
OK
```

Expand Down
4 changes: 2 additions & 2 deletions airtable.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ require 'airtable/version'
Gem::Specification.new do |spec|
spec.name = 'airtable'
spec.version = Airtable::VERSION
spec.authors = ['Nathan Esquenazi', 'Alexander Sorokin']
spec.email = ['[email protected]', '[email protected]']
spec.authors = ['Nathan Esquenazi', 'Alexander Sorokin', 'Oleksandr Simonov']
spec.email = ['[email protected]', '[email protected]', '[email protected]']
spec.summary = 'Easily connect to airtable data using ruby'
spec.description = 'Easily connect to airtable data using ruby with access to all of the airtable features.'
spec.homepage = 'https://github.com/nesquena/airtable-ruby'
Expand Down
59 changes: 48 additions & 11 deletions lib/airtable/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Airtable
class CLI
def initialize(args)
trap_interrupt
@operation = args.shift
@args = args
@options = {}
@parser = OptionParser.new
Expand All @@ -30,7 +31,7 @@ def start
end

def add_banner
@parser.banner = 'Usage: airtable [options]'
@parser.banner = 'Usage: airtable operation options'
@parser.separator ''
end

Expand Down Expand Up @@ -70,27 +71,57 @@ def add_tail_options
def add_supported_operations
@parser.separator ''
@parser.separator 'Supported Operations:'
@parser.separator "\tGet Record (if only RECORD_ID provided)"
@parser.separator "\tGet Field (if RECORD_ID and FIELD_ID are provided)"
@parser.separator "\tUpdate Field (if RECORD_ID, FIELD_ID and VALUE are provided)"
@parser.separator "\tget - Get Record/Field"
@parser.separator "\tupdate - Update Field"
@parser.separator ''
@parser.separator 'Examples:'
@parser.separator "\tairtable get -B Base -t Table"
@parser.separator "\tairtable get -B Base -t Table -r RECORD_ID"
@parser.separator "\tairtable get -B Base -t Table -f FIELD_NAME"
@parser.separator "\tairtable get -B Base -t Table -f FIELD_NAME -r RECORD_ID"
@parser.separator "\tairtable update -b Base -t table -r RECORD_ID -f FIELD_NAME -v newValue"
@parser.separator ''
end

def valid_options?
@options[:table_name] && !@options[:table_name].empty? &&
@options[:base_id] && !@options[:base_id].empty? &&
@options[:record_id] && !@options[:record_id].empty?
@options[:base_id] && !@options[:base_id].empty?
end

def record_id?
@options[:record_id] && !@options[:record_id].empty?
end

def field_name?
@options[:field_name] && !@options[:field_name].empty?
end

def run_operation
if @options[:field_value] && !@options[:field_value].empty? && @options[:field_name] && !@options[:field_name].empty?
case @operation
when "get"
case
when record_id? && field_name?
print_field
when record_id? && !field_name?
print_record
when !record_id? && field_name?
print_fields
else
print_records
end
when "update"
[:field_value, :field_name, :record_id].each do |key|
return if !@options[key] || @options[key].empty?
end
update_field
elsif @options[:field_name] && !@options[:field_name].empty?
print_field
else
print_record
end
end

def print_records
puts (::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).select.map do |record|
{ id: record.id, fields: record.fields, createdTime: record.created_at }
end).to_json
end

def print_record
record = ::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).find(@options[:record_id])
Expand All @@ -102,6 +133,12 @@ def print_field
puts record.fields[@options[:field_name]]
end

def print_fields
::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).select.each do |record|
puts record.fields[@options[:field_name]]
end
end

def update_field
::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name])
.update(@options[:record_id], @options[:field_name] => @options[:field_value])
Expand Down
2 changes: 1 addition & 1 deletion lib/airtable/entity/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def table(name)
end

def __make_request__(method, path, data)
url = [::Airtable.server_url, @id, path].join('/')
url = [::Airtable.server_url, CGI.escape(@id), path].join('/')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@id shouldn't need escaping for now. It can only be 17-character alphanumeric starting with "app".

resp = ::Airtable::Request.new(url, data, @client.api_key)
.request(method)
if resp.success?
Expand Down
28 changes: 13 additions & 15 deletions lib/airtable/entity/record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ class Record
extend Forwardable
attr_reader :id, :created_at, :fields

def_delegators :@fields, :[], :[]=

def initialize(id, options = {})
@id = id
parse_options(options)
Expand All @@ -18,15 +16,15 @@ def new_record?
@id.nil? || @id.empty?
end

def __create__(base, name)
res = base.__make_request__(:post, name, fields: fields)
def __create__(base, table_name)
res = base.__make_request__(:post, table_name, fields: fields)
@id = res['id']
parse_options(fields: res['fields'], created_at: res['createdTime'])
self
end

def __update__(base, name)
args = [:patch, [name, @id].join('/'), fields: fields]
def __update__(base, table_name)
args = [:patch, [table_name, @id].join('/'), fields: fields]
res = base.__make_request__(*args)
parse_options(fields: res['fields'])
self
Expand All @@ -38,14 +36,14 @@ def __fetch__(base, path)
self
end

def __replace__(base, name)
res = base.__make_request__(:put, [name, @id].join('/'), fields: fields)
def __replace__(base, table_name)
res = base.__make_request__(:put, [table_name, @id].join('/'), fields: fields)
parse_options(fields: res['fields'])
self
end

def __destroy__(base, name)
res = base.__make_request__(:delete, [name, @id].join('/'), {})
def __destroy__(base, table_name)
res = base.__make_request__(:delete, [table_name, @id].join('/'), {})
res['deleted']
end

Expand All @@ -58,24 +56,24 @@ def []=(key, value)
end

class << self
def all(base, name, params)
def all(base, table_name, params)
res = []
__fetch__(base, name, params, res)
__fetch__(base, table_name, params, res)
res
end

private

def __fetch__(base, name, params, res)
result = base.__make_request__(:get, name, params)
def __fetch__(base, table_name, params, res)
result = base.__make_request__(:get, table_name, params)
result['records'].each do |r|
args = [
r['id'], fields: r['fields'], created_at: r['createdTime']
]
res << new(*args)
end
return unless result['offset']
__fetch__(base, name, params.merge(offset: result['offset']), res)
__fetch__(base, table_name, params.merge(offset: result['offset']), res)
end
end

Expand Down
43 changes: 28 additions & 15 deletions lib/airtable/entity/table.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'cgi'
module Airtable
module Entity
# Airtable Table entity
Expand All @@ -6,9 +7,9 @@ class Table
DEFAULT_DIRECTION = 'asc'.freeze

def initialize(base, name)
@name = name
@name = CGI.escape(name)
Copy link
Contributor

@syrnick syrnick Jan 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think CGI.escape works - we need to use an equivalent of encodeUriComponent in JS .

3cc21ea

For example, they differ in handling of spaces - GCI.escape turns them into '+' v.s. %2B.

Separately, storing @name as encoded might be confusing later on. If someone does p table.name, they would n't expect the encoding. It'd be better to either:
a) have an accessor url_encoded_name that does encoding on the fly.
b) wrap name access in url_encode e.g. @table_path = [@base, url_encode(@name)]
c) add in instance variable @url_encoded_name = url_encode(@name)

@base = base
@args = [@base, @name]
@table_path = [@base, @name]
end

def select(options = {})
Expand All @@ -20,24 +21,24 @@ def select(options = {})
end

def find(id)
args = [@base, [@name, id].join('/')]
::Airtable::Entity::Record.new(id).__fetch__(*args)
table_path = [@base, [@name, id].join('/')]
::Airtable::Entity::Record.new(id).__fetch__(*table_path)
end

def create(fields)
::Airtable::Entity::Record.new(nil, fields: fields).__create__(*@args)
::Airtable::Entity::Record.new(nil, fields: fields).__create__(*@table_path)
end

def update(id, fields)
::Airtable::Entity::Record.new(id, fields: fields).__update__(*@args)
::Airtable::Entity::Record.new(id, fields: fields).__update__(*@table_path)
end

def replace(id, fields)
::Airtable::Entity::Record.new(id, fields: fields).__replace__(*@args)
::Airtable::Entity::Record.new(id, fields: fields).__replace__(*@table_path)
end

def destroy(id)
::Airtable::Entity::Record.new(id).__destroy__(*@args)
::Airtable::Entity::Record.new(id).__destroy__(*@table_path)
end

private
Expand All @@ -51,16 +52,18 @@ def fetch_records(params)
end

def update_default_params(params, options)
params[:fields] = option_value_for(options, :fields)
params[:maxRecords] = option_value_for(options, :max_records)
params[:offset] = option_value_for(options, :offset)
params[:pageSize] = option_value_for(options, :limit) || PAGE_SIZE
params[:fields] = option_value_for(options, :fields)
params[:maxRecords] = option_value_for(options, :max_records)
params[:offset] = option_value_for(options, :offset)
params[:view] = option_value_for(options, :view)
params[:filterByFormula] = option_value_for(options, :filter_by_formula)
params[:pageSize] = option_value_for(options, :limit) || PAGE_SIZE
end

def validate_params(params)
if params[:fields] && !params[:fields].is_a?(::Array)
raise ::Airtable::FieldsOptionError
end
validate_fields(params[:fields])
param_not_empty?(params[:view], ::Airtable::ViewOptionError)
param_not_empty?(params[:filterByFormula], ::Airtable::FilterByFormulaOptionError)
raise ::Airtable::LimitOptionError if params[:pageSize].to_i <= 0
# rubocop:disable all
if params[:maxRecords] && params[:maxRecords].to_i <= 0
Expand All @@ -69,6 +72,16 @@ def validate_params(params)
# rubocop:enable all
end

def validate_fields(value)
return if !value || value.is_a?(::Array)
raise ::Airtable::FieldsOptionError
end

def param_not_empty?(value, klass)
return if !value || (!value.empty? && value.is_a?(::String))
raise klass
end

def update_sort_options(params, options)
sort_option = option_value_for(options, :sort)
case sort_option
Expand Down
Loading