airtable-orm 0.2.1 → 0.2.2

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: dc3056cbf98026ea8a89b01d99c632ad01da304d85841544552b44565f897793
4
- data.tar.gz: cf839f12c5812c4834604dc46c716fdb671c47110d94829fa863d82f5d56e489
3
+ metadata.gz: be3a4c1ecff6395abd45d69731ac804de74bf2fdd06127c2c4b5a9d3c9417c6b
4
+ data.tar.gz: 8dbb4c9e8a0c087a09d64685e7c974a044c0cea9b3f4eb554a952492065181ff
5
5
  SHA512:
6
- metadata.gz: dfcd829b2a0df28d151770f30c0f51e98680bc6ab0c6f8fd5f0ff9fd2fd6ffefdef4f3c30fbe7dbcb3f3afa08689d6dffb6fc234d28a4403c3ec56f004f0abfa
7
- data.tar.gz: 6c31d239c24e6c86e6303cc5eb7fdd34a5916c914c27999625c6b0c945178f9d7def6386855ba77cc13493054d14dc8abf1f087ca27474b8ff6a21f1e34f2e57
6
+ metadata.gz: c09d85ccb058caa27c77d568c82c8e72eed4950a610e358278994eed7c0456ef45798f125ee6729a65a903b865ae167e7180a5b1a26db1107c79e0c549beceb9
7
+ data.tar.gz: 6727276b68304892c7df6e896bcb1117efbc0a4266da3fe40e5ce9f0b4c46f8327e964f3ada9537b6093ea92fbe4ed4485c3abccaa94aa4af38093c335e7cde2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.2] - 2026-09-24
4
+
5
+ ### Fixed
6
+
7
+ - `find` raises `RecordNotFound` for a deleted/nonexistent record ID. Airtable answers such a
8
+ GET with `403 INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND`, not 404, so it used to surface as a
9
+ plain `ApiError`. Because a revoked token or lost table permission returns the same 403, `find`
10
+ now disambiguates with one probe of the table (a one-record, no-fields list request): readable
11
+ → `RecordNotFound`, otherwise the original `ApiError` is re-raised; a `ConnectionError` during
12
+ the probe propagates. The 404 mapping is unchanged. `reload` and `belongs_to` (which reads a
13
+ stale link as `nil`) benefit accordingly.
14
+ - `ApiError` is raised (instead of a `TypeError`) for Airtable's bare string error body such as
15
+ `{"error": "NOT_FOUND"}`, which previously broke the 404 → `RecordNotFound` mapping in `find`.
16
+
3
17
  ## [0.2.1] - 2026-07-15
4
18
 
5
19
  ### Fixed
@@ -66,11 +66,13 @@ module Airtable
66
66
  ERB::Util.url_encode(string)
67
67
  end
68
68
 
69
- # Parse an Airtable API error response and raise an ApiError.
69
+ # Parse an Airtable API error response and raise an ApiError. Airtable sends either
70
+ # {"error" => {"type", "message"}} or a bare {"error" => "NOT_FOUND"} (e.g. on 404).
70
71
  def self.raise_api_error(status, error)
71
- type = (error.is_a?(Hash) && error.dig("error", "type")) || "Communication error"
72
+ detail = error.is_a?(Hash) ? error["error"] : nil
73
+ type = (detail.is_a?(Hash) ? detail["type"] : detail) || "Communication error"
72
74
  msg = case error
73
- when Hash then error.dig("error", "message")
75
+ when Hash then detail.is_a?(Hash) ? detail["message"] : nil
74
76
  when String then error
75
77
  when NilClass then "invalid or empty response body (not valid JSON)"
76
78
  else error.inspect
@@ -74,7 +74,9 @@ module Airtable
74
74
  Airtable::ORM::Http::Client.raise_api_error(response.status, parsed_response)
75
75
  end
76
76
  rescue Airtable::ORM::ApiError => e
77
- raise Airtable::ORM::RecordNotFound, "Couldn't find record with id=#{id}" if e.status == 404
77
+ if e.status == 404 || (missing_record_forbidden?(e) && table_readable?)
78
+ raise Airtable::ORM::RecordNotFound, "Couldn't find record with id=#{id}"
79
+ end
78
80
 
79
81
  raise
80
82
  end
@@ -145,6 +147,22 @@ module Airtable
145
147
 
146
148
  private
147
149
 
150
+ # Airtable answers a GET for a nonexistent (e.g. deleted) record ID with this 403, not
151
+ # 404 — the same response a token without access to the table/base gets.
152
+ def missing_record_forbidden?(error)
153
+ error.status == 403 && error.response.is_a?(Hash) &&
154
+ error.response.dig("error", "type") == "INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND"
155
+ end
156
+
157
+ # Disambiguates that 403 with one cheap list request (one record, no fields): readable
158
+ # table → the record is gone. Never map the 403 blindly — a revoked token or lost
159
+ # permission would then look like "every record deleted" to the host. A ConnectionError
160
+ # still propagates (retryable, not evidence either way).
161
+ def table_readable?
162
+ response = client.connection.post(api_path("listRecords"), { maxRecords: 1, fields: [] })
163
+ response.success?
164
+ end
165
+
148
166
  # Send a single batch PATCH request to Airtable.
149
167
  def send_batch_update(batch, result)
150
168
  body = {
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Airtable
4
4
  module ORM
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airtable-orm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Sklenar