norairrecord 0.2.1 → 0.4.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/README.md +7 -1
- data/lib/norairrecord/client.rb +1 -1
- data/lib/norairrecord/table.rb +134 -28
- data/lib/norairrecord/util.rb +24 -0
- data/lib/norairrecord/version.rb +1 -1
- data/lib/norairrecord.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d6cb01e5b17df2640cfc26b6e719deb69d04b21cbb89eb8f31b4dffc052c998
|
4
|
+
data.tar.gz: 66406d3ffbcda723cdc524447555d646e58578b8ea56b7c25f3170bfb754eb93
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 352e8b49946a4b54657140b5736c246f56f9381c8e6225176674bb93438b6d25b9e9f3ba2a60596996760a5adf6be5d8318cf98951102f09cd0d6a19e990cd9a
|
7
|
+
data.tar.gz: 6570f39459f17d523fb963c4274c8d8e3831b9281659e257c44c45d799556d6c3df10c733061ed6ec1dc5dbdbea56733d7f7c9ed85b0c2b2cc686f5821ac2536
|
data/README.md
CHANGED
@@ -52,4 +52,10 @@ stuff not in the OG:
|
|
52
52
|
* `Norairrecord::RecordNotFoundError`
|
53
53
|
* never again wonder if an error is because you goofed up an ID or you're getting ratelimited
|
54
54
|
* `where` argument on `has_many` lookups
|
55
|
-
* `Table#first`, `Table#first_where`
|
55
|
+
* `Table#first`, `Table#first_where`
|
56
|
+
* you're not gonna believe it:
|
57
|
+
* `Table.batch_`{update,upsert,create,save}
|
58
|
+
* makes ratelimits much less painful
|
59
|
+
* `Util` (those little methods we have to keep writing again and again, now in one place)
|
60
|
+
* custom RPS limit
|
61
|
+
* `Norairrecord.rps_limit = 3`
|
data/lib/norairrecord/client.rb
CHANGED
@@ -23,7 +23,7 @@ module Norairrecord
|
|
23
23
|
},
|
24
24
|
) do |conn|
|
25
25
|
if Norairrecord.throttle?
|
26
|
-
conn.request :airrecord_rate_limiter, requests_per_second: AIRTABLE_RPS_LIMIT
|
26
|
+
conn.request :airrecord_rate_limiter, requests_per_second: Norairrecord.rps_limit || AIRTABLE_RPS_LIMIT
|
27
27
|
end
|
28
28
|
conn.adapter :net_http_persistent
|
29
29
|
end
|
data/lib/norairrecord/table.rb
CHANGED
@@ -2,9 +2,13 @@ require 'rubygems' # For Gem::Version
|
|
2
2
|
|
3
3
|
module Norairrecord
|
4
4
|
class Table
|
5
|
+
BATCH_SIZE = 10
|
6
|
+
|
5
7
|
class << self
|
6
8
|
attr_writer :api_key, :base_key, :table_name
|
7
9
|
|
10
|
+
include Norairrecord::Util
|
11
|
+
|
8
12
|
def base_key
|
9
13
|
@base_key || (superclass < Table ? superclass.base_key : nil)
|
10
14
|
end
|
@@ -13,6 +17,16 @@ module Norairrecord
|
|
13
17
|
@table_name || (superclass < Table ? superclass.table_name : nil)
|
14
18
|
end
|
15
19
|
|
20
|
+
|
21
|
+
# finds the actual parent class of a (possibly) subtype class
|
22
|
+
def responsible_class
|
23
|
+
if @base_key
|
24
|
+
self.class
|
25
|
+
else
|
26
|
+
superclass < Table ? superclass.responsible_class : nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
16
30
|
def client
|
17
31
|
@@clients ||= {}
|
18
32
|
@@clients[api_key] ||= Client.new(api_key)
|
@@ -64,9 +78,8 @@ module Norairrecord
|
|
64
78
|
def find_many(ids, where: nil, sort: nil)
|
65
79
|
return [] if ids.empty?
|
66
80
|
|
67
|
-
|
68
|
-
formula =
|
69
|
-
formula = "AND(#{formula},#{where})" if where
|
81
|
+
formula = any_of(ids.map { |id| "RECORD_ID() = '#{id}'" })
|
82
|
+
formula = all_of(formula, where) if where
|
70
83
|
records(filter: formula, sort:).sort_by { |record| or_args.index(record.id) }
|
71
84
|
end
|
72
85
|
|
@@ -87,7 +100,6 @@ module Norairrecord
|
|
87
100
|
end
|
88
101
|
end
|
89
102
|
|
90
|
-
|
91
103
|
def create(fields, options = {})
|
92
104
|
new(fields).tap { |record| record.save(options) }
|
93
105
|
end
|
@@ -97,7 +109,7 @@ module Norairrecord
|
|
97
109
|
clazz = self
|
98
110
|
st = @subtype_mapping[fields[@subtype_column]]
|
99
111
|
raise Norairrecord::UnknownTypeError, "#{fields[@subtype_column]}?????" if @subtype_strict && st.nil?
|
100
|
-
clazz =
|
112
|
+
clazz = Kernel.const_get(st) if st
|
101
113
|
clazz.new(fields, id:, created_at:)
|
102
114
|
else
|
103
115
|
self.new(fields, id: id, created_at: created_at)
|
@@ -125,22 +137,19 @@ module Norairrecord
|
|
125
137
|
parsed_response = client.parse(response.body)
|
126
138
|
|
127
139
|
if response.success?
|
128
|
-
records = parsed_response["records"]
|
129
|
-
records.map! { |record|
|
130
|
-
self.new_with_subtype(record["fields"], id: record["id"], created_at: record["createdTime"])
|
131
|
-
}
|
140
|
+
records = map_new parsed_response["records"]
|
132
141
|
|
133
142
|
if paginate && parsed_response["offset"]
|
134
143
|
records.concat(records(
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
+
filter: filter,
|
145
|
+
sort: sort,
|
146
|
+
view: view,
|
147
|
+
paginate: paginate,
|
148
|
+
fields: fields,
|
149
|
+
offset: parsed_response["offset"],
|
150
|
+
max_records: max_records,
|
151
|
+
page_size: page_size,
|
152
|
+
))
|
144
153
|
end
|
145
154
|
|
146
155
|
records
|
@@ -161,10 +170,105 @@ module Norairrecord
|
|
161
170
|
records(**options.merge(filter:))
|
162
171
|
end
|
163
172
|
|
164
|
-
|
165
|
-
|
173
|
+
def map_new(arr)
|
174
|
+
arr.map do |record|
|
175
|
+
self.new_with_subtype(record["fields"], id: record["id"], created_at: record["createdTime"])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def batch_update(recs, options = {})
|
180
|
+
res = []
|
181
|
+
recs.each_slice(BATCH_SIZE) do |chunk|
|
182
|
+
body = {
|
183
|
+
records: chunk.map do |record|
|
184
|
+
{
|
185
|
+
fields: record.update_hash,
|
186
|
+
id: record.id,
|
187
|
+
}
|
188
|
+
end,
|
189
|
+
**options
|
190
|
+
}.to_json
|
191
|
+
|
192
|
+
response = client.connection.patch("v0/#{base_key}/#{client.escape(table_name)}", body, { 'Content-Type' => 'application/json' })
|
193
|
+
parsed_response = client.parse(response.body)
|
194
|
+
if response.success?
|
195
|
+
res.concat(parsed_response["records"])
|
196
|
+
else
|
197
|
+
client.handle_error(response.status, parsed_response)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
map_new res
|
201
|
+
end
|
202
|
+
|
203
|
+
def batch_upsert(recs, merge_fields, options = {}, include_ids: nil, hydrate: false)
|
204
|
+
merge_fields = Array(merge_fields) # allows passing in a single field
|
205
|
+
|
206
|
+
created, updated, records = [], [], []
|
166
207
|
|
208
|
+
recs.each_slice(BATCH_SIZE) do |chunk|
|
209
|
+
body = {
|
210
|
+
records: chunk.map { |rec| { fields: rec.fields, id: (include_ids ? rec.id : nil) }.compact },
|
211
|
+
**options,
|
212
|
+
performUpsert: { fieldsToMergeOn: merge_fields }
|
213
|
+
}.to_json
|
167
214
|
|
215
|
+
response = client.connection.patch("v0/#{base_key}/#{client.escape(table_name)}", body, { 'Content-Type' => 'application/json' })
|
216
|
+
parsed_response = response.success? ? client.parse(response.body) : client.handle_error(response.status, client.parse(response.body))
|
217
|
+
|
218
|
+
if response.success?
|
219
|
+
created.concat(parsed_response.fetch('createdRecords', []))
|
220
|
+
updated.concat(parsed_response.fetch('updatedRecords', []))
|
221
|
+
records.concat(parsed_response.fetch('records', []))
|
222
|
+
else
|
223
|
+
client.handle_error(response.status, parsed_response)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
if hydrate && records.any?
|
228
|
+
record_hash = records.map { |record| [record["id"], self.new_with_subtype(record["fields"], id: record["id"], created_at: record["createdTime"])] }.to_h
|
229
|
+
|
230
|
+
created.map! { |id| record_hash[id] }.compact!
|
231
|
+
updated.map! { |id| record_hash[id] }.compact!
|
232
|
+
records = record_hash.values
|
233
|
+
end
|
234
|
+
|
235
|
+
{ created:, updated:, records: }
|
236
|
+
end
|
237
|
+
|
238
|
+
def batch_create(recs, options = {})
|
239
|
+
records = []
|
240
|
+
recs.each_slice(BATCH_SIZE) do |chunk|
|
241
|
+
body = {
|
242
|
+
records: chunk.map { |record| { fields: record.serializable_fields } },
|
243
|
+
**options
|
244
|
+
}.to_json
|
245
|
+
|
246
|
+
response = client.connection.post("v0/#{base_key}/#{client.escape(table_name)}", body, { 'Content-Type' => 'application/json' })
|
247
|
+
parsed_response = client.parse(response.body)
|
248
|
+
|
249
|
+
if response.success?
|
250
|
+
records.concat(parsed_response["records"])
|
251
|
+
else
|
252
|
+
client.handle_error(response.status, parsed_response)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
map_new records
|
256
|
+
end
|
257
|
+
|
258
|
+
def upsert(fields, merge_fields, options = {})
|
259
|
+
record = batch_upsert([self.new(fields)], merge_fields, options)&.dig(:records, 0)
|
260
|
+
record ? new(record) : nil
|
261
|
+
end
|
262
|
+
|
263
|
+
def batch_save(records)
|
264
|
+
res = []
|
265
|
+
to_be_created, to_be_updated = records.partition &:new_record?
|
266
|
+
res.concat(batch_create(to_be_created))
|
267
|
+
res.concat(batch_update(to_be_updated))
|
268
|
+
end
|
269
|
+
|
270
|
+
alias all records
|
271
|
+
end
|
168
272
|
|
169
273
|
attr_reader :fields, :id, :created_at, :updated_keys
|
170
274
|
|
@@ -201,10 +305,10 @@ module Norairrecord
|
|
201
305
|
fields[key] = value
|
202
306
|
end
|
203
307
|
|
204
|
-
def patch(
|
205
|
-
|
206
|
-
return @fields if
|
207
|
-
@fields.merge!(self.class.update(self.id,
|
308
|
+
def patch(updates = {}, options = {})
|
309
|
+
updates.reject! { |key, value| @fields[key] == value }
|
310
|
+
return @fields if updates.empty? # don't hit AT if we don't have real changes
|
311
|
+
@fields.merge!(self.class.update(self.id, updates, options).reject { |key, _| updated_keys.include?(key) })
|
208
312
|
end
|
209
313
|
|
210
314
|
def create(options = {})
|
@@ -230,11 +334,13 @@ module Norairrecord
|
|
230
334
|
def save(options = {})
|
231
335
|
return create(options) if new_record?
|
232
336
|
return true if @updated_keys.empty?
|
337
|
+
self.fields = self.class.update(self.id, self.update_hash, options)
|
338
|
+
end
|
233
339
|
|
234
|
-
|
340
|
+
def update_hash
|
341
|
+
Hash[@updated_keys.map { |key|
|
235
342
|
[key, fields[key]]
|
236
343
|
}]
|
237
|
-
self.fields = self.class.update(self.id, update_hash, options)
|
238
344
|
end
|
239
345
|
|
240
346
|
def destroy
|
@@ -255,7 +361,7 @@ module Norairrecord
|
|
255
361
|
end
|
256
362
|
|
257
363
|
def comment(text)
|
258
|
-
response = client.connection.post("v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}/comments", {text:}.to_json, { 'Content-Type' => 'application/json' })
|
364
|
+
response = client.connection.post("v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}/comments", { text: }.to_json, { 'Content-Type' => 'application/json' })
|
259
365
|
parsed_response = client.parse(response.body)
|
260
366
|
|
261
367
|
if response.success?
|
@@ -273,6 +379,7 @@ module Norairrecord
|
|
273
379
|
self.class == other.class &&
|
274
380
|
serializable_fields == other.serializable_fields
|
275
381
|
end
|
382
|
+
|
276
383
|
alias eql? ==
|
277
384
|
|
278
385
|
def hash
|
@@ -307,7 +414,6 @@ module Norairrecord
|
|
307
414
|
result
|
308
415
|
end
|
309
416
|
|
310
|
-
|
311
417
|
protected
|
312
418
|
|
313
419
|
def fields=(fields)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Norairrecord
|
2
|
+
module Util
|
3
|
+
class << self
|
4
|
+
def all_of(*args)
|
5
|
+
"AND(#{args.join(',')})"
|
6
|
+
end
|
7
|
+
def any_of(*args)
|
8
|
+
"OR(#{args.join(',')})"
|
9
|
+
end
|
10
|
+
def none_of(*args)
|
11
|
+
"NOT(#{all_of(*args)})"
|
12
|
+
end
|
13
|
+
def field_is_any(field, *args)
|
14
|
+
any_of(*args.map { |arg| "#{field}='#{sanitize(arg)}'"})
|
15
|
+
end
|
16
|
+
def sanitize(arg)
|
17
|
+
arg.gsub(/['"]/, '\\\\\0')
|
18
|
+
end
|
19
|
+
def mass_sanitize(*args)
|
20
|
+
args.map { |arg| sanitize(arg) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/norairrecord/version.rb
CHANGED
data/lib/norairrecord.rb
CHANGED
@@ -2,13 +2,14 @@ require "json"
|
|
2
2
|
require "faraday"
|
3
3
|
require 'faraday/net_http_persistent'
|
4
4
|
require "time"
|
5
|
+
require "norairrecord/util"
|
5
6
|
require "norairrecord/version"
|
6
7
|
require "norairrecord/client"
|
7
8
|
require "norairrecord/table"
|
8
9
|
|
9
10
|
module Norairrecord
|
10
11
|
extend self
|
11
|
-
attr_accessor :api_key, :throttle, :base_url, :user_agent
|
12
|
+
attr_accessor :api_key, :throttle, :base_url, :user_agent, :rps_limit
|
12
13
|
|
13
14
|
Error = Class.new(StandardError)
|
14
15
|
UnknownTypeError = Class.new(Error)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: norairrecord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- nora
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -107,6 +107,7 @@ files:
|
|
107
107
|
- lib/norairrecord/client.rb
|
108
108
|
- lib/norairrecord/faraday_rate_limiter.rb
|
109
109
|
- lib/norairrecord/table.rb
|
110
|
+
- lib/norairrecord/util.rb
|
110
111
|
- lib/norairrecord/version.rb
|
111
112
|
- norairrecord.gemspec
|
112
113
|
homepage: https://github.com/24c02/norairrecord
|