airrecord 0.2.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|