norairrecord 0.2.0 → 0.3.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: a5f9d1c3992d715d55a1308918a574a00b83042da2e58bfbcc9389ff51e0b6cd
4
- data.tar.gz: d4372b35b219e5ffd6384c42e3687dd9019dabbb48d8bb622d395eb8aadab90a
3
+ metadata.gz: 0f5fc9903052fc25fe8bdd071a9989ab92561387d0dabc70ad82045a8335eab1
4
+ data.tar.gz: d445e59398dbb6c10e08ce645ad12f2dd8dd763ca16334f4ca93477ebe8c9d37
5
5
  SHA512:
6
- metadata.gz: 0fdcca5fc8ee492d692fe1271d60052d82ddf4c18ff4ec275e464cf9871dd52cf9996e393ac01292bc780a31dc0681c92af1485282e6b11138fea35957ae1b18
7
- data.tar.gz: f023f648cb134d4de8c0773fd1a2d0882393ea7109af105f2cd9b23fde4d1eeb9d519ee695ac341912a191d3d6a3b6b946efb3d7c8bc6d60ecdaf387dacfcef5
6
+ metadata.gz: 6c78431764c880fbf87535717d223b9ce805c7b6a38a45a64dcf649b073c6dae6d4d322a7130a59d5f0e3cef51389887858568f260a83f34c8cca6e66ef1f3d8
7
+ data.tar.gz: c97892cf8cb38a5ac9103e6e5358c4993e3fcca8f0e5e42a8d68ac9e22daad3d83320dcf9cc8592e46bd26965ff394fac9fbfdce385d341682aa6f6ab73467eb
data/README.md CHANGED
@@ -26,6 +26,7 @@ stuff not in the OG:
26
26
  * custom endpoint URL
27
27
  * handy for inspecting/ratelimiting
28
28
  * `Norairrecord.base_url = "https://somewhere_else"`
29
+ * or `ENV['AIRTABLE_ENDPOINT_URL']`
29
30
  * custom UA
30
31
  * `Norairrecord.user_agent = "i'm the reason why you're getting 429s!"`
31
32
  * `Table#airtable_url`
@@ -51,4 +52,7 @@ stuff not in the OG:
51
52
  * `Norairrecord::RecordNotFoundError`
52
53
  * never again wonder if an error is because you goofed up an ID or you're getting ratelimited
53
54
  * `where` argument on `has_many` lookups
54
- * `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
@@ -16,7 +16,7 @@ module Norairrecord
16
16
 
17
17
  def connection
18
18
  @connection ||= Faraday.new(
19
- url: Norairrecord.base_url || "https://api.airtable.com",
19
+ url: Norairrecord.base_url || ENV['AIRTABLE_ENDPOINT_URL'] || "https://api.airtable.com",
20
20
  headers: {
21
21
  "Authorization" => "Bearer #{api_key}",
22
22
  "User-Agent" => Norairrecord.user_agent || "Airrecord (nora's version)/#{Norairrecord::VERSION}",
@@ -2,6 +2,8 @@ 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
 
@@ -64,7 +66,7 @@ module Norairrecord
64
66
  def find_many(ids, where: nil, sort: nil)
65
67
  return [] if ids.empty?
66
68
 
67
- or_args = ids.map { |id| "RECORD_ID() = '#{id}'"}.join(',')
69
+ or_args = ids.map { |id| "RECORD_ID() = '#{id}'" }.join(',')
68
70
  formula = "OR(#{or_args})"
69
71
  formula = "AND(#{formula},#{where})" if where
70
72
  records(filter: formula, sort:).sort_by { |record| or_args.index(record.id) }
@@ -87,7 +89,6 @@ module Norairrecord
87
89
  end
88
90
  end
89
91
 
90
-
91
92
  def create(fields, options = {})
92
93
  new(fields).tap { |record| record.save(options) }
93
94
  end
@@ -97,7 +98,7 @@ module Norairrecord
97
98
  clazz = self
98
99
  st = @subtype_mapping[fields[@subtype_column]]
99
100
  raise Norairrecord::UnknownTypeError, "#{fields[@subtype_column]}?????" if @subtype_strict && st.nil?
100
- clazz = Kernel.const_get(st) if st
101
+ clazz = Kernel.const_get(st) if st
101
102
  clazz.new(fields, id:, created_at:)
102
103
  else
103
104
  self.new(fields, id: id, created_at: created_at)
@@ -125,22 +126,19 @@ module Norairrecord
125
126
  parsed_response = client.parse(response.body)
126
127
 
127
128
  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
- }
129
+ records = map_new parsed_response["records"]
132
130
 
133
131
  if paginate && parsed_response["offset"]
134
132
  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
- ))
133
+ filter: filter,
134
+ sort: sort,
135
+ view: view,
136
+ paginate: paginate,
137
+ fields: fields,
138
+ offset: parsed_response["offset"],
139
+ max_records: max_records,
140
+ page_size: page_size,
141
+ ))
144
142
  end
145
143
 
146
144
  records
@@ -161,10 +159,105 @@ module Norairrecord
161
159
  records(**options.merge(filter:))
162
160
  end
163
161
 
164
- alias all records
165
- end
162
+ def map_new(arr)
163
+ arr.map do |record|
164
+ self.new_with_subtype(record["fields"], id: record["id"], created_at: record["createdTime"])
165
+ end
166
+ end
167
+
168
+ def batch_update(recs, options = {})
169
+ res = []
170
+ recs.each_slice(BATCH_SIZE) do |chunk|
171
+ body = {
172
+ records: chunk.map do |record|
173
+ {
174
+ fields: record.update_hash,
175
+ id: record.id,
176
+ }
177
+ end,
178
+ **options
179
+ }.to_json
180
+
181
+ response = client.connection.patch("v0/#{base_key}/#{client.escape(table_name)}", body, { 'Content-Type' => 'application/json' })
182
+ parsed_response = client.parse(response.body)
183
+ if response.success?
184
+ res.concat(parsed_response["records"])
185
+ else
186
+ client.handle_error(response.status, parsed_response)
187
+ end
188
+ end
189
+ map_new res
190
+ end
191
+
192
+ def batch_upsert(recs, merge_fields, options = {}, include_ids: nil, hydrate: false)
193
+ merge_fields = Array(merge_fields) # allows passing in a single field
166
194
 
195
+ created, updated, records = [], [], []
167
196
 
197
+ recs.each_slice(BATCH_SIZE) do |chunk|
198
+ body = {
199
+ records: chunk.map { |rec| { fields: rec.fields, id: (include_ids ? rec.id : nil) }.compact },
200
+ **options,
201
+ performUpsert: { fieldsToMergeOn: merge_fields }
202
+ }.to_json
203
+
204
+ response = client.connection.patch("v0/#{base_key}/#{client.escape(table_name)}", body, { 'Content-Type' => 'application/json' })
205
+ parsed_response = response.success? ? client.parse(response.body) : client.handle_error(response.status, client.parse(response.body))
206
+
207
+ if response.success?
208
+ created.concat(parsed_response.fetch('createdRecords', []))
209
+ updated.concat(parsed_response.fetch('updatedRecords', []))
210
+ records.concat(parsed_response.fetch('records', []))
211
+ else
212
+ client.handle_error(response.status, parsed_response)
213
+ end
214
+ end
215
+
216
+ if hydrate && records.any?
217
+ record_hash = records.map { |record| [record["id"], self.new_with_subtype(record["fields"], id: record["id"], created_at: record["createdTime"])] }.to_h
218
+
219
+ created.map! { |id| record_hash[id] }.compact!
220
+ updated.map! { |id| record_hash[id] }.compact!
221
+ records = record_hash.values
222
+ end
223
+
224
+ { created:, updated:, records: }
225
+ end
226
+
227
+ def batch_create(recs, options = {})
228
+ records = []
229
+ recs.each_slice(BATCH_SIZE) do |chunk|
230
+ body = {
231
+ records: chunk.map { |record| { fields: record.serializable_fields } },
232
+ **options
233
+ }.to_json
234
+
235
+ response = client.connection.post("v0/#{base_key}/#{client.escape(table_name)}", body, { 'Content-Type' => 'application/json' })
236
+ parsed_response = client.parse(response.body)
237
+
238
+ if response.success?
239
+ records.concat(parsed_response["records"])
240
+ else
241
+ client.handle_error(response.status, parsed_response)
242
+ end
243
+ end
244
+ map_new records
245
+ end
246
+
247
+ def upsert(fields, merge_fields, options = {})
248
+ record = batch_upsert([self.new(fields)], merge_fields, options)&.dig(:records, 0)
249
+ record ? new(record) : nil
250
+ end
251
+
252
+ def batch_save(records)
253
+ res = []
254
+ to_be_created, to_be_updated = records.partition &:new_record?
255
+ res.concat(batch_create(to_be_created))
256
+ res.concat(batch_update(to_be_updated))
257
+ end
258
+
259
+ alias all records
260
+ end
168
261
 
169
262
  attr_reader :fields, :id, :created_at, :updated_keys
170
263
 
@@ -201,10 +294,10 @@ module Norairrecord
201
294
  fields[key] = value
202
295
  end
203
296
 
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) })
297
+ def patch(updates = {}, options = {})
298
+ updates.reject! { |key, value| @fields[key] == value }
299
+ return @fields if updates.empty? # don't hit AT if we don't have real changes
300
+ @fields.merge!(self.class.update(self.id, updates, options).reject { |key, _| updated_keys.include?(key) })
208
301
  end
209
302
 
210
303
  def create(options = {})
@@ -230,11 +323,13 @@ module Norairrecord
230
323
  def save(options = {})
231
324
  return create(options) if new_record?
232
325
  return true if @updated_keys.empty?
326
+ self.fields = self.class.update(self.id, self.update_hash, options)
327
+ end
233
328
 
234
- update_hash = Hash[@updated_keys.map { |key|
329
+ def update_hash
330
+ Hash[@updated_keys.map { |key|
235
331
  [key, fields[key]]
236
332
  }]
237
- self.fields = self.class.update(self.id, update_hash, options)
238
333
  end
239
334
 
240
335
  def destroy
@@ -255,7 +350,7 @@ module Norairrecord
255
350
  end
256
351
 
257
352
  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' })
353
+ 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
354
  parsed_response = client.parse(response.body)
260
355
 
261
356
  if response.success?
@@ -273,6 +368,7 @@ module Norairrecord
273
368
  self.class == other.class &&
274
369
  serializable_fields == other.serializable_fields
275
370
  end
371
+
276
372
  alias eql? ==
277
373
 
278
374
  def hash
@@ -283,7 +379,7 @@ module Norairrecord
283
379
  def transaction(&block)
284
380
  txn_updates = {}
285
381
 
286
- singleton_class.alias_method :original_setter, :[]=
382
+ singleton_class.define_method(:original_setter, method(:[]=))
287
383
 
288
384
  define_singleton_method(:[]=) do |key, value|
289
385
  txn_updates[key] = value
@@ -301,13 +397,12 @@ module Norairrecord
301
397
  rescue => e
302
398
  raise
303
399
  ensure
304
- singleton_class.alias_method :[]=, :original_setter
305
- singleton_class.remove_method :original_setter
400
+ singleton_class.define_method(:[]=, method(:original_setter))
401
+ singleton_class.remove_method(:original_setter)
306
402
  end
307
403
  result
308
404
  end
309
405
 
310
-
311
406
  protected
312
407
 
313
408
  def fields=(fields)
@@ -1,3 +1,3 @@
1
1
  module Norairrecord
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
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.0
4
+ version: 0.3.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-01-24 00:00:00.000000000 Z
11
+ date: 2025-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday