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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2413a6757fd7aa66b83507ab161d8305bd233b29739652cc0c05302df835ed0
4
- data.tar.gz: 468978e1acae10b550e887affa319a24a6fa4a41ab6200b0193d36866d31364c
3
+ metadata.gz: 2d6cb01e5b17df2640cfc26b6e719deb69d04b21cbb89eb8f31b4dffc052c998
4
+ data.tar.gz: 66406d3ffbcda723cdc524447555d646e58578b8ea56b7c25f3170bfb754eb93
5
5
  SHA512:
6
- metadata.gz: 26ee966a87a85da8c545c9ab8a49c6d654dd2fdb33891e21954b13eedbd1041c9a18e03ba06cf903a7930800166cbb8d7906db3c20ad8ea6dc92d5e250e7f68a
7
- data.tar.gz: 696a61147478f307b86e7adceb7464ccbc1a023756a076feadc4240f27e6e2482ea9289c47b38e9995e255df20f66da16b1a675e4599baf41a54e04bc58acb01
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`
@@ -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
@@ -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
- or_args = ids.map { |id| "RECORD_ID() = '#{id}'"}.join(',')
68
- formula = "OR(#{or_args})"
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 = Kernel.const_get(st) if st
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
- filter: filter,
136
- sort: sort,
137
- view: view,
138
- paginate: paginate,
139
- fields: fields,
140
- offset: parsed_response["offset"],
141
- max_records: max_records,
142
- page_size: page_size,
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
- alias all records
165
- end
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(update_hash = {}, options = {})
205
- update_hash.reject! { |key, value| @fields[key] == value }
206
- return @fields if update_hash.empty? # don't hit AT if we don't have real changes
207
- @fields.merge!(self.class.update(self.id, update_hash, options).reject { |key, _| updated_keys.include?(key) })
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
- update_hash = Hash[@updated_keys.map { |key|
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
@@ -1,3 +1,3 @@
1
1
  module Norairrecord
2
- VERSION = "0.2.1"
2
+ VERSION = "0.4.0"
3
3
  end
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.2.1
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-02-03 00:00:00.000000000 Z
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