airrecord 0.2.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -3
- data/README.md +74 -17
- data/lib/airrecord.rb +6 -0
- data/lib/airrecord/client.rb +19 -5
- data/lib/airrecord/faraday_rate_limiter.rb +60 -0
- data/lib/airrecord/query_string.rb +47 -0
- data/lib/airrecord/table.rb +46 -107
- data/lib/airrecord/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52a1efc847fab74c1bd0ff89814a274b1eb789359efda52a71675b6e7602f362
|
4
|
+
data.tar.gz: cb766b78e31e88ed253c2f7d7f2f7a1166fb5fec61bdbf46602b20b76a43cf9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee40920b905e08644f8de12241b3bd842bac654b396c71e68677d8eec53b6f67f2fec17841754c9841e61542ac9f5de83c420141efc198cc2309d7d4fbbf0629
|
7
|
+
data.tar.gz: 64de96b6d7b81a8b9e147aa6adbf81402a5a6c1c5e23b681807df3cd408e420dfe065beb15135ad63706bf5899539cd979792afac9c8971aa21281fb55cce9d5
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,22 @@
|
|
1
|
-
# 1.0.0
|
2
|
-
|
3
|
-
* 1.0.0
|
1
|
+
# 1.0.0
|
2
|
+
|
3
|
+
* 1.0.0 introduces breaking changes, including removing support for symbols and
|
4
|
+
implementing associations as instance methods. To upgrade:
|
5
|
+
1. Change snake-case symbols to their correct column names:
|
6
|
+
`record["First Name"]` instead of `record[:first_name]`)
|
7
|
+
2. Change your association calls to use instance methods instead of `[]`:
|
8
|
+
```ruby
|
9
|
+
class Tea < Airrecord::Table
|
10
|
+
has_many :brews, class: "Brew", column: "Brews"
|
11
|
+
end
|
12
|
+
tea[:brews] #=> Error, no longer supported
|
13
|
+
tea.brews #=> [<Brew>, <Brew>] returns associated Brew instances
|
14
|
+
tea["Brews"] #=> ["rec456", "rec789"] returns a raw Airtable field
|
15
|
+
```
|
16
|
+
3. Dates that are formed `\d{4}-\d{2}-\d{2}` are no longer auto-parsed. Define a helper instead.
|
17
|
+
* Automatically throttle client calls to Airtable's API limit of 5 requests per second.
|
18
|
+
* Fix sorting by multiple fields
|
19
|
+
* Report `User-Agent` as `Airrecord`.
|
4
20
|
|
5
21
|
# 0.2.5
|
6
22
|
|
data/README.md
CHANGED
@@ -20,7 +20,7 @@ class Tea < Airrecord::Table
|
|
20
20
|
self.base_key = "app1"
|
21
21
|
self.table_name = "Teas"
|
22
22
|
|
23
|
-
has_many
|
23
|
+
has_many :brews, class: "Brew", column: "Brews"
|
24
24
|
|
25
25
|
def self.chinese
|
26
26
|
all(filter: '{Country} = "China"')
|
@@ -43,14 +43,14 @@ class Brew < Airrecord::Table
|
|
43
43
|
self.base_key = "app1"
|
44
44
|
self.table_name = "Brews"
|
45
45
|
|
46
|
-
belongs_to
|
46
|
+
belongs_to :tea, class: "Tea", column: "Tea"
|
47
47
|
|
48
48
|
def self.hot
|
49
49
|
all(filter: "{Temperature} > 90")
|
50
50
|
end
|
51
51
|
|
52
52
|
def done_brewing?
|
53
|
-
self["Created At"] + self["Duration"] > Time.now
|
53
|
+
Time.parse(self["Created At"]) + self["Duration"] > Time.now
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
@@ -58,7 +58,7 @@ teas = Tea.all
|
|
58
58
|
tea = teas.first
|
59
59
|
tea["Country"] # access atribute
|
60
60
|
tea.location # instance methods
|
61
|
-
tea
|
61
|
+
tea.brews # associated brews
|
62
62
|
```
|
63
63
|
|
64
64
|
A short-hand API for definitions and more ad-hoc querying is also available:
|
@@ -115,10 +115,17 @@ end
|
|
115
115
|
This gives us a class that maps to records in a table. Class methods are
|
116
116
|
available to fetch records on the table.
|
117
117
|
|
118
|
+
### Reading a Single Record
|
119
|
+
|
120
|
+
Retrieve a single record via `#find`:
|
121
|
+
```ruby
|
122
|
+
tea = Tea.find("someid")
|
123
|
+
```
|
124
|
+
|
118
125
|
### Listing Records
|
119
126
|
|
120
|
-
Retrieval of multiple records is done through `#all`. To get all records
|
121
|
-
table:
|
127
|
+
Retrieval of multiple records is usually done through `#all`. To get all records
|
128
|
+
in a table:
|
122
129
|
|
123
130
|
```ruby
|
124
131
|
Tea.all # array of Tea instances
|
@@ -183,17 +190,33 @@ Tea.all(paginate: false)
|
|
183
190
|
Tea.all(sort: { "Created At" => "desc" }, paginate: false)
|
184
191
|
```
|
185
192
|
|
193
|
+
When you know the IDs of the records you want, and you want them in an ad-hoc
|
194
|
+
order, use `#find_many` instead of `#all`:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
teas = Tea.find_many(["someid", "anotherid", "yetanotherid"])
|
198
|
+
#=> [<Tea @id="someid">,<Tea @id="anotherid">, <Tea @id="yetanotherid">]
|
199
|
+
```
|
200
|
+
|
186
201
|
### Creating
|
187
202
|
|
188
|
-
Creating a new record is done through
|
203
|
+
Creating a new record is done through `Table.create`.
|
189
204
|
|
190
205
|
```ruby
|
191
|
-
tea = Tea.
|
192
|
-
tea.create # creates the record
|
206
|
+
tea = Tea.create("Name" => "Feng Gang", "Type" => "Green", "Country" => "China")
|
193
207
|
tea.id # id of the new record
|
194
208
|
tea["Name"] # "Feng Gang"
|
195
209
|
```
|
196
210
|
|
211
|
+
If you need to manipulate a record before saving it, you can use `Table.new`
|
212
|
+
instead of `create`, then call `#save` when you're ready.
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
tea = Tea.new("Type" => "Green", "Country" => "China")
|
216
|
+
tea["Name"] = "Feng Gang"
|
217
|
+
tea.save
|
218
|
+
```
|
219
|
+
|
197
220
|
Note that column names need to match the exact column names in Airtable,
|
198
221
|
otherwise Airrecord will throw an error that no column matches it.
|
199
222
|
|
@@ -268,14 +291,22 @@ class Tea < Airrecord::Table
|
|
268
291
|
self.base_key = "app1"
|
269
292
|
self.table_name = "Teas"
|
270
293
|
|
271
|
-
has_many
|
294
|
+
has_many :brews, class: "Brew", column: "Brews"
|
295
|
+
has_one :teapot, class: "Teapot", column: "Teapot"
|
272
296
|
end
|
273
297
|
|
274
298
|
class Brew < Airrecord::Table
|
275
299
|
self.base_key = "app1"
|
276
300
|
self.table_name = "Brews"
|
277
301
|
|
278
|
-
belongs_to
|
302
|
+
belongs_to :tea, class: "Tea", column: "Tea"
|
303
|
+
end
|
304
|
+
|
305
|
+
class Teapot < Airrecord::Table
|
306
|
+
self.base_key = "app1"
|
307
|
+
self.table_name = "Teapot"
|
308
|
+
|
309
|
+
belongs_to :tea, class: "Tea", column: "Tea"
|
279
310
|
end
|
280
311
|
```
|
281
312
|
|
@@ -289,15 +320,23 @@ _not_ support associations across Bases.
|
|
289
320
|
To retrieve records from associations to a record:
|
290
321
|
|
291
322
|
```ruby
|
292
|
-
tea = Tea.find(
|
293
|
-
|
323
|
+
tea = Tea.find("rec123")
|
324
|
+
|
325
|
+
# record.association returns Airrecord instances
|
326
|
+
tea.brews #=> [<Brew @id="rec456">, <Brew @id="rec789">]
|
327
|
+
tea.teapot #=> <Teapot @id="rec012">
|
328
|
+
|
329
|
+
# record["Associated Column"] returns the raw Airtable field, an array of IDs
|
330
|
+
tea["Brews"] #=> ["rec789", "rec456"]
|
331
|
+
tea["Teapot"] #=> ["rec012"]
|
294
332
|
```
|
295
333
|
|
296
334
|
This in turn works the other way too:
|
297
335
|
|
298
336
|
```ruby
|
299
|
-
brew = Brew.find(
|
300
|
-
brew
|
337
|
+
brew = Brew.find("rec456")
|
338
|
+
brew.tea #=> <Tea @id="rec123"> the associated tea instance
|
339
|
+
brew["Tea"] #=> the raw Airtable field, a single-item array ["rec123"]
|
301
340
|
```
|
302
341
|
|
303
342
|
### Creating associated records
|
@@ -305,9 +344,18 @@ brew["Tea"] # the associated tea instance
|
|
305
344
|
You can easily associate records with each other:
|
306
345
|
|
307
346
|
```ruby
|
308
|
-
tea = Tea.find(
|
347
|
+
tea = Tea.find("rec123")
|
309
348
|
# This will create a brew associated with the specific tea
|
310
|
-
brew = Brew.new("
|
349
|
+
brew = Brew.new("Temperature" => "80", "Time" => "4m", "Rating" => "5")
|
350
|
+
brew.tea = tea
|
351
|
+
brew.create
|
352
|
+
```
|
353
|
+
|
354
|
+
Alternatively, you can specify association ids directly:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
tea = Tea.find("rec123")
|
358
|
+
brew = Brew.new("Tea" => [tea.id], "Temperature" => "80", "Time" => "4m", "Rating" => "5")
|
311
359
|
brew.create
|
312
360
|
```
|
313
361
|
|
@@ -328,6 +376,15 @@ end
|
|
328
376
|
Tea.find("rec3838")
|
329
377
|
```
|
330
378
|
|
379
|
+
### Throttling
|
380
|
+
|
381
|
+
Airtable's API enforces a 5 requests per second limit per client. In most cases,
|
382
|
+
you won't reach this limit in a single thread but it's possible for some fast
|
383
|
+
calls. Airrecord automatically enforces this limit. It doesn't use a naive
|
384
|
+
throttling algorithm that does a `sleep(0.2)` between each call, but rather
|
385
|
+
keeps a sliding window of when each call was made and will sleep at the end of
|
386
|
+
the window if requires. This means that bursting is supported.
|
387
|
+
|
331
388
|
### Production Middlewares
|
332
389
|
|
333
390
|
For production use-cases, it's worth considering adding retries and circuit
|
data/lib/airrecord.rb
CHANGED
data/lib/airrecord/client.rb
CHANGED
@@ -1,25 +1,39 @@
|
|
1
1
|
require 'uri'
|
2
|
+
require_relative 'query_string'
|
3
|
+
require_relative 'faraday_rate_limiter'
|
2
4
|
|
3
5
|
module Airrecord
|
4
6
|
class Client
|
5
7
|
attr_reader :api_key
|
6
8
|
attr_writer :connection
|
7
9
|
|
10
|
+
# Per Airtable's documentation you will get throttled for 30 seconds if you
|
11
|
+
# issue more than 5 requests per second. Airrecord is a good citizen.
|
12
|
+
AIRTABLE_RPS_LIMIT = 5
|
13
|
+
|
8
14
|
def initialize(api_key)
|
9
15
|
@api_key = api_key
|
10
16
|
end
|
11
17
|
|
12
18
|
def connection
|
13
|
-
@connection ||= Faraday.new(
|
14
|
-
|
15
|
-
|
16
|
-
|
19
|
+
@connection ||= Faraday.new(
|
20
|
+
url: "https://api.airtable.com",
|
21
|
+
headers: {
|
22
|
+
"Authorization" => "Bearer #{api_key}",
|
23
|
+
"User-Agent" => "Airrecord/#{Airrecord::VERSION}",
|
24
|
+
"X-API-VERSION" => "0.1.0",
|
25
|
+
},
|
26
|
+
request: { params_encoder: Airrecord::QueryString },
|
27
|
+
) { |conn|
|
28
|
+
if Airrecord.throttle?
|
29
|
+
conn.request :airrecord_rate_limiter, requests_per_second: AIRTABLE_RPS_LIMIT
|
30
|
+
end
|
17
31
|
conn.adapter :net_http_persistent
|
18
32
|
}
|
19
33
|
end
|
20
34
|
|
21
35
|
def escape(*args)
|
22
|
-
|
36
|
+
QueryString.escape(*args)
|
23
37
|
end
|
24
38
|
|
25
39
|
def parse(body)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Airrecord
|
4
|
+
class FaradayRateLimiter < Faraday::Middleware
|
5
|
+
class << self
|
6
|
+
attr_accessor :requests
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(app, requests_per_second: nil, sleeper: nil)
|
10
|
+
super(app)
|
11
|
+
@rps = requests_per_second
|
12
|
+
@sleeper = sleeper || ->(seconds) { sleep(seconds) }
|
13
|
+
@mutex = Mutex.new
|
14
|
+
clear
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
@mutex.synchronize do
|
19
|
+
wait if too_many_requests_in_last_second?
|
20
|
+
@app.call(env).on_complete do |_response_env|
|
21
|
+
requests << Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
22
|
+
requests.shift if requests.size > @rps
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear
|
28
|
+
self.class.requests = []
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def requests
|
34
|
+
self.class.requests
|
35
|
+
end
|
36
|
+
|
37
|
+
def too_many_requests_in_last_second?
|
38
|
+
return false unless @rps
|
39
|
+
return false unless requests.size >= @rps
|
40
|
+
window_span < 1.0
|
41
|
+
end
|
42
|
+
|
43
|
+
def wait
|
44
|
+
# Time to wait until making the next request to stay within limits.
|
45
|
+
# [1.1, 1.2, 1.3, 1.4, 1.5] => 1 - 0.4 => 0.6
|
46
|
+
wait_time = 1.0 - window_span
|
47
|
+
@sleeper.call(wait_time)
|
48
|
+
end
|
49
|
+
|
50
|
+
# [1.1, 1.2, 1.3, 1.4, 1.5] => 1.5 - 1.1 => 0.4
|
51
|
+
def window_span
|
52
|
+
requests.last - requests.first
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Faraday::Request.register_middleware(
|
58
|
+
# Avoid polluting the global middleware namespace with a prefix.
|
59
|
+
:airrecord_rate_limiter => Airrecord::FaradayRateLimiter
|
60
|
+
)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Airrecord
|
4
|
+
# Airtable expects that arrays in query strings be encoded with indices.
|
5
|
+
# Faraday follows Rack conventions and encodes arrays _without_ indices.
|
6
|
+
#
|
7
|
+
# Airrecord::QueryString is a Faraday-compliant params_encoder that follows
|
8
|
+
# the Airtable spec.
|
9
|
+
module QueryString
|
10
|
+
def self.encode(params)
|
11
|
+
params.map { |key, val| Encodings[val].call(key, val) }.join('&')
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.decode(query)
|
15
|
+
Faraday::NestedParamsEncoder.decode(query)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.escape(*query)
|
19
|
+
query.map { |qs| ERB::Util.url_encode(qs) }.join('')
|
20
|
+
end
|
21
|
+
|
22
|
+
module Encodings
|
23
|
+
using QueryString
|
24
|
+
|
25
|
+
def self.[](value)
|
26
|
+
TYPES.fetch(value.class, DEFAULT)
|
27
|
+
end
|
28
|
+
|
29
|
+
TYPES = {
|
30
|
+
Array => lambda { |prefix, array|
|
31
|
+
array.each_with_index.map do |value, index|
|
32
|
+
self[value].call("#{prefix}[#{index}]", value)
|
33
|
+
end
|
34
|
+
},
|
35
|
+
Hash => lambda { |prefix, hash|
|
36
|
+
hash.map do |key, value|
|
37
|
+
self[value].call("#{prefix}[#{key}]", value)
|
38
|
+
end
|
39
|
+
},
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
DEFAULT = lambda do |key, value|
|
43
|
+
"#{QueryString.escape(key)}=#{QueryString.escape(value)}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/airrecord/table.rb
CHANGED
@@ -1,47 +1,33 @@
|
|
1
1
|
module Airrecord
|
2
|
-
# TODO: This would be much simplified if we had a schema instead. Hopefully
|
3
|
-
# one day Airtable will add this, but to simplify and crush the majority of
|
4
|
-
# the bugs that hide in here (which would be related to the dynamic schema) we
|
5
|
-
# may just query the first page and infer a schema from there that can be
|
6
|
-
# overridden on the specific classes.
|
7
|
-
#
|
8
|
-
# Right now I bet there's a bunch of bugs around similar named column keys (in
|
9
|
-
# terms of capitalization), it's inconsistent and non-obvious that `create`
|
10
|
-
# doesn't use the same column keys as everything else.
|
11
|
-
#
|
12
|
-
# 2018-11-01
|
13
|
-
# deprecate_symbols: long-term plan is to force everyone to use raw strings,
|
14
|
-
# to match the Airtable behavior. For now we'll just warn when using symbols
|
15
|
-
# with a deprecation notice.
|
16
|
-
|
17
2
|
class Table
|
18
|
-
def deprecate_symbols
|
19
|
-
self.class.deprecate_symbols
|
20
|
-
end
|
21
|
-
|
22
3
|
class << self
|
23
4
|
attr_accessor :base_key, :table_name, :api_key, :associations
|
24
5
|
|
25
|
-
def deprecate_symbols
|
26
|
-
warn Kernel.caller.first + ": warning: Using symbols with airrecord is deprecated."
|
27
|
-
end
|
28
|
-
|
29
6
|
def client
|
30
7
|
@@clients ||= {}
|
31
8
|
@@clients[api_key] ||= Client.new(api_key)
|
32
9
|
end
|
33
10
|
|
34
|
-
def has_many(
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
11
|
+
def has_many(method_name, options)
|
12
|
+
define_method(method_name.to_sym) do
|
13
|
+
# Get association ids in reverse order, because Airtable’s UI and API
|
14
|
+
# sort associations in opposite directions. We want to match the UI.
|
15
|
+
ids = (self[options.fetch(:column)] || []).reverse
|
16
|
+
table = Kernel.const_get(options.fetch(:class))
|
17
|
+
options[:single] ? table.find(ids.first) : table.find_many(ids)
|
18
|
+
end
|
19
|
+
|
20
|
+
define_method("#{method_name}=".to_sym) do |value|
|
21
|
+
self[options.fetch(:column)] = Array(value).map(&:id).reverse
|
22
|
+
end
|
39
23
|
end
|
40
24
|
|
41
|
-
def belongs_to(
|
42
|
-
has_many(
|
25
|
+
def belongs_to(method_name, options)
|
26
|
+
has_many(method_name, options.merge(single: true))
|
43
27
|
end
|
44
28
|
|
29
|
+
alias has_one belongs_to
|
30
|
+
|
45
31
|
def api_key
|
46
32
|
@api_key || Airrecord.api_key
|
47
33
|
end
|
@@ -57,13 +43,22 @@ module Airrecord
|
|
57
43
|
end
|
58
44
|
end
|
59
45
|
|
46
|
+
def find_many(ids)
|
47
|
+
or_args = ids.map { |id| "RECORD_ID() = '#{id}'"}.join(',')
|
48
|
+
formula = "OR(#{or_args})"
|
49
|
+
records(filter: formula).sort_by { |record| or_args.index(record.id) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def create(fields)
|
53
|
+
new(fields).tap { |record| record.save }
|
54
|
+
end
|
55
|
+
|
60
56
|
def records(filter: nil, sort: nil, view: nil, offset: nil, paginate: true, fields: nil, max_records: nil, page_size: nil)
|
61
57
|
options = {}
|
62
58
|
options[:filterByFormula] = filter if filter
|
63
59
|
|
64
60
|
if sort
|
65
61
|
options[:sort] = sort.map { |field, direction|
|
66
|
-
deprecate_symbols if field.is_a? Symbol
|
67
62
|
{ field: field.to_s, direction: direction }
|
68
63
|
}
|
69
64
|
end
|
@@ -105,7 +100,7 @@ module Airrecord
|
|
105
100
|
alias_method :all, :records
|
106
101
|
end
|
107
102
|
|
108
|
-
attr_reader :fields, :
|
103
|
+
attr_reader :fields, :id, :created_at, :updated_keys
|
109
104
|
|
110
105
|
def initialize(fields, id: nil, created_at: nil)
|
111
106
|
@id = id
|
@@ -118,51 +113,22 @@ module Airrecord
|
|
118
113
|
end
|
119
114
|
|
120
115
|
def [](key)
|
121
|
-
|
122
|
-
|
123
|
-
if fields[key]
|
124
|
-
deprecate_symbols if key.is_a? Symbol
|
125
|
-
value = fields[key]
|
126
|
-
elsif column_mappings[key]
|
127
|
-
deprecate_symbols if key.is_a? Symbol
|
128
|
-
value = fields[column_mappings[key]]
|
129
|
-
end
|
130
|
-
|
131
|
-
if association = self.association(key)
|
132
|
-
klass = Kernel.const_get(association[:class])
|
133
|
-
associations = value.map { |id_or_obj|
|
134
|
-
id_or_obj = id_or_obj.respond_to?(:id) ? id_or_obj.id : id_or_obj
|
135
|
-
klass.find(id_or_obj)
|
136
|
-
}
|
137
|
-
return associations.first if association[:single]
|
138
|
-
associations
|
139
|
-
else
|
140
|
-
type_cast(value)
|
141
|
-
end
|
116
|
+
validate_key(key)
|
117
|
+
fields[key]
|
142
118
|
end
|
143
119
|
|
144
120
|
def []=(key, value)
|
145
|
-
|
146
|
-
if fields[key]
|
147
|
-
|
148
|
-
|
149
|
-
fields[key] = value
|
150
|
-
elsif column_mappings[key]
|
151
|
-
deprecate_symbols
|
152
|
-
return if fields[column_mappings[key]] == value # no-op
|
153
|
-
@updated_keys << column_mappings[key]
|
154
|
-
fields[column_mappings[key]] = value
|
155
|
-
else
|
156
|
-
@updated_keys << key
|
157
|
-
fields[key] = value
|
158
|
-
end
|
121
|
+
validate_key(key)
|
122
|
+
return if fields[key] == value # no-op
|
123
|
+
@updated_keys << key
|
124
|
+
fields[key] = value
|
159
125
|
end
|
160
126
|
|
161
127
|
def create
|
162
128
|
raise Error, "Record already exists (record has an id)" unless new_record?
|
163
129
|
|
164
130
|
body = { fields: serializable_fields }.to_json
|
165
|
-
response = client.connection.post("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}", body, { 'Content-Type'
|
131
|
+
response = client.connection.post("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}", body, { 'Content-Type' => 'application/json' })
|
166
132
|
parsed_response = client.parse(response.body)
|
167
133
|
|
168
134
|
if response.success?
|
@@ -175,7 +141,7 @@ module Airrecord
|
|
175
141
|
end
|
176
142
|
|
177
143
|
def save
|
178
|
-
|
144
|
+
return create if new_record?
|
179
145
|
|
180
146
|
return true if @updated_keys.empty?
|
181
147
|
|
@@ -186,11 +152,11 @@ module Airrecord
|
|
186
152
|
}]
|
187
153
|
}.to_json
|
188
154
|
|
189
|
-
response = client.connection.patch("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}", body, { 'Content-Type'
|
155
|
+
response = client.connection.patch("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}", body, { 'Content-Type' => 'application/json' })
|
190
156
|
parsed_response = client.parse(response.body)
|
191
157
|
|
192
158
|
if response.success?
|
193
|
-
self.fields = parsed_response
|
159
|
+
self.fields = parsed_response["fields"]
|
194
160
|
else
|
195
161
|
client.handle_error(response.status, parsed_response)
|
196
162
|
end
|
@@ -209,18 +175,8 @@ module Airrecord
|
|
209
175
|
end
|
210
176
|
end
|
211
177
|
|
212
|
-
def serializable_fields
|
213
|
-
|
214
|
-
if association(key)
|
215
|
-
value = [ value ] unless value.is_a?(Enumerable)
|
216
|
-
assocs = value.map { |assoc|
|
217
|
-
assoc.respond_to?(:id) ? assoc.id : assoc
|
218
|
-
}
|
219
|
-
[key, assocs]
|
220
|
-
else
|
221
|
-
[key, value]
|
222
|
-
end
|
223
|
-
}]
|
178
|
+
def serializable_fields
|
179
|
+
fields
|
224
180
|
end
|
225
181
|
|
226
182
|
def ==(other)
|
@@ -236,28 +192,11 @@ module Airrecord
|
|
236
192
|
|
237
193
|
protected
|
238
194
|
|
239
|
-
def association(key)
|
240
|
-
if self.class.associations
|
241
|
-
self.class.associations.find { |association|
|
242
|
-
association[:column].to_s == column_mappings[key].to_s || association[:column].to_s == key.to_s
|
243
|
-
}
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
195
|
def fields=(fields)
|
248
196
|
@updated_keys = []
|
249
|
-
@column_mappings = Hash[fields.keys.map { |key| [underscore(key), key] }] # TODO remove (deprecate_symbols)
|
250
197
|
@fields = fields
|
251
198
|
end
|
252
199
|
|
253
|
-
def self.underscore(key) # TODO remove (deprecate_symbols)
|
254
|
-
key.to_s.strip.gsub(/\W+/, "_").downcase.to_sym
|
255
|
-
end
|
256
|
-
|
257
|
-
def underscore(key) # TODO remove (deprecate_symbols)
|
258
|
-
self.class.underscore(key)
|
259
|
-
end
|
260
|
-
|
261
200
|
def created_at=(created_at)
|
262
201
|
return unless created_at
|
263
202
|
@created_at = Time.parse(created_at)
|
@@ -267,14 +206,14 @@ module Airrecord
|
|
267
206
|
self.class.client
|
268
207
|
end
|
269
208
|
|
270
|
-
def
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
209
|
+
def validate_key(key)
|
210
|
+
return true unless key.is_a?(Symbol)
|
211
|
+
raise Error, <<~MSG
|
212
|
+
Airrecord 1.0 dropped support for Symbols as field names.
|
213
|
+
Please use the full column name, a String, instead.
|
214
|
+
You might try: record['#{key.to_s.gsub('_', ' ')}']
|
215
|
+
MSG
|
276
216
|
end
|
277
|
-
|
278
217
|
end
|
279
218
|
|
280
219
|
def self.table(api_key, base_key, table_name)
|
data/lib/airrecord/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: airrecord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Simon Eskildsen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-11-
|
11
|
+
date: 2018-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -113,6 +113,8 @@ files:
|
|
113
113
|
- bin/setup
|
114
114
|
- lib/airrecord.rb
|
115
115
|
- lib/airrecord/client.rb
|
116
|
+
- lib/airrecord/faraday_rate_limiter.rb
|
117
|
+
- lib/airrecord/query_string.rb
|
116
118
|
- lib/airrecord/table.rb
|
117
119
|
- lib/airrecord/version.rb
|
118
120
|
homepage: https://github.com/sirupsen/airrecord
|