airtable-orm 0.1.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 +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +121 -0
- data/lib/airtable/orm/associations.rb +153 -0
- data/lib/airtable/orm/attributes.rb +236 -0
- data/lib/airtable/orm/base.rb +33 -0
- data/lib/airtable/orm/batch_result.rb +28 -0
- data/lib/airtable/orm/collection.rb +46 -0
- data/lib/airtable/orm/configuration.rb +70 -0
- data/lib/airtable/orm/core.rb +49 -0
- data/lib/airtable/orm/errors.rb +62 -0
- data/lib/airtable/orm/http/client.rb +85 -0
- data/lib/airtable/orm/http/error_handler.rb +25 -0
- data/lib/airtable/orm/http/rate_limiter.rb +52 -0
- data/lib/airtable/orm/persistence.rb +361 -0
- data/lib/airtable/orm/querying.rb +183 -0
- data/lib/airtable/orm/schema.rb +66 -0
- data/lib/airtable/orm/testing/stub_helpers.rb +53 -0
- data/lib/airtable/orm/testing.rb +3 -0
- data/lib/airtable/orm/version.rb +7 -0
- data/lib/airtable/orm.rb +47 -0
- data/lib/airtable-orm.rb +4 -0
- metadata +131 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airtable
|
|
4
|
+
module ORM
|
|
5
|
+
module Querying
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
DEFAULT_PAGE_SIZE = 100
|
|
9
|
+
DEFAULT_MAX_RECORDS = 1000
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
# Fetch all records from the table. Always ignores max_records; use +where+ for limits.
|
|
13
|
+
def all(**options)
|
|
14
|
+
where(**options, max_records: nil)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Query records with a formula
|
|
18
|
+
def where(formula: nil, sort: nil, view: nil, fields: nil, max_records: DEFAULT_MAX_RECORDS,
|
|
19
|
+
page_size: DEFAULT_PAGE_SIZE)
|
|
20
|
+
records(formula:, sort:, view:, fields:, max_records:, page_size:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Fetch the first record matching the criteria
|
|
24
|
+
def first(formula: nil, sort: nil)
|
|
25
|
+
where(formula:, sort:, max_records: 1).first
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Fetch the last record matching the criteria by reversing the sort order.
|
|
29
|
+
# NOTE: Unlike ActiveRecord, Airtable has no default sort order.
|
|
30
|
+
# Calling last without sort returns an arbitrary record (same as first).
|
|
31
|
+
def last(formula: nil, sort: nil)
|
|
32
|
+
where(formula:, sort: reverse_sort_order(sort), max_records: 1).first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Find the first record matching attribute conditions.
|
|
36
|
+
# Example: Airtable::Case.find_by(slug: "abc-123")
|
|
37
|
+
def find_by(**conditions)
|
|
38
|
+
raise ArgumentError, "find_by requires at least one condition" if conditions.empty?
|
|
39
|
+
|
|
40
|
+
clauses = conditions.map do |field, value|
|
|
41
|
+
field_id = resolve_field_id(field)
|
|
42
|
+
"{#{field_id}} = #{format_formula_value(value)}"
|
|
43
|
+
end
|
|
44
|
+
formula = clauses.size == 1 ? clauses.first : "AND(#{clauses.join(", ")})"
|
|
45
|
+
first(formula: formula)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Like find_by but raises RecordNotFound when no match.
|
|
49
|
+
def find_by!(**conditions)
|
|
50
|
+
find_by(**conditions) || raise(Airtable::ORM::RecordNotFound,
|
|
51
|
+
"Couldn't find #{name} with #{conditions.inspect}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Count records without instantiating model objects.
|
|
55
|
+
# Requests no fields to minimize payload (Airtable has no count-only endpoint).
|
|
56
|
+
def count(formula: nil)
|
|
57
|
+
count_records(formula:)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Format a Ruby value for use in an Airtable formula comparison.
|
|
61
|
+
# Returns the correctly typed formula literal (quoted string, bare number, function call, etc.).
|
|
62
|
+
def format_formula_value(value)
|
|
63
|
+
case value
|
|
64
|
+
when NilClass then "BLANK()"
|
|
65
|
+
when TrueClass then "TRUE()"
|
|
66
|
+
when FalseClass then "FALSE()"
|
|
67
|
+
when Integer, Float then value.to_s
|
|
68
|
+
when Date then "'#{value.iso8601}'"
|
|
69
|
+
when Time, DateTime then "'#{value.utc.iso8601}'"
|
|
70
|
+
else "'#{escape_formula_value(value)}'"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Escape a value for use in an Airtable formula string literal.
|
|
75
|
+
# Handles backslashes, single quotes, and control characters.
|
|
76
|
+
def escape_formula_value(value)
|
|
77
|
+
value.to_s
|
|
78
|
+
.gsub("\\", "\\\\\\\\") # backslashes first
|
|
79
|
+
.gsub("'", "\\\\'") # single quotes
|
|
80
|
+
.gsub("\n", "\\n") # newlines
|
|
81
|
+
.gsub("\r", "\\r") # carriage returns
|
|
82
|
+
.gsub("\t", "\\t") # tabs
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Fetch records from the API
|
|
88
|
+
def records(formula: nil, sort: nil, view: nil, fields: nil, max_records: nil, page_size: nil, offset: nil,
|
|
89
|
+
paginate: true)
|
|
90
|
+
all_records = []
|
|
91
|
+
|
|
92
|
+
each_page(formula:, sort:, view:, fields:, max_records:, page_size:, offset:, paginate:) do |page|
|
|
93
|
+
all_records.concat(page.map { |record_data| instantiate_from_api_response(record_data) })
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Airtable::ORM::Collection.new(all_records, model_class: self)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Count records without instantiating model objects.
|
|
100
|
+
def count_records(formula: nil)
|
|
101
|
+
total = 0
|
|
102
|
+
each_page(formula:, fields: []) { |page| total += page.size }
|
|
103
|
+
total
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Paginate through listRecords, yielding each page's raw records array.
|
|
107
|
+
def each_page(formula: nil, sort: nil, view: nil, fields: nil, max_records: nil, page_size: nil, offset: nil,
|
|
108
|
+
paginate: true)
|
|
109
|
+
current_offset = offset
|
|
110
|
+
|
|
111
|
+
loop do
|
|
112
|
+
options = build_query_options(formula:, sort:, view:, fields:, max_records:, page_size:,
|
|
113
|
+
offset: current_offset)
|
|
114
|
+
response = client.connection.post(api_path("listRecords"), options)
|
|
115
|
+
parsed_response = response.body
|
|
116
|
+
|
|
117
|
+
Airtable::ORM::Http::Client.raise_api_error(response.status, parsed_response) unless response.success?
|
|
118
|
+
|
|
119
|
+
yield parsed_response["records"]
|
|
120
|
+
|
|
121
|
+
break unless paginate && parsed_response["offset"]
|
|
122
|
+
|
|
123
|
+
current_offset = parsed_response["offset"]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Build query options for the API
|
|
128
|
+
def build_query_options(formula:, sort:, view:, fields:, max_records:, page_size:, offset:)
|
|
129
|
+
{
|
|
130
|
+
returnFieldsByFieldId: true,
|
|
131
|
+
filterByFormula: formula,
|
|
132
|
+
sort: sort && normalize_sort_options(sort),
|
|
133
|
+
view: view,
|
|
134
|
+
fields: fields && resolve_field_ids(fields),
|
|
135
|
+
maxRecords: max_records,
|
|
136
|
+
pageSize: page_size,
|
|
137
|
+
offset: offset
|
|
138
|
+
}.compact
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Normalize sort options to Airtable API format
|
|
142
|
+
# Accepts: { field: :asc } or [[:field1, :desc], [:field2, :asc]]
|
|
143
|
+
# Converts symbol attributes to their Airtable field IDs
|
|
144
|
+
def normalize_sort_options(sort)
|
|
145
|
+
return [] unless sort.respond_to?(:map)
|
|
146
|
+
|
|
147
|
+
sort.map { |field, direction| { field: resolve_field_id(field), direction: direction.to_s } }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Resolve symbol attribute to Airtable field ID
|
|
151
|
+
# If the field is already a string, return it as-is (assuming it's a valid field ID)
|
|
152
|
+
# If it's a symbol, look up the field ID from the attribute map and validate it exists
|
|
153
|
+
def resolve_field_id(field)
|
|
154
|
+
return field.to_s unless field.is_a?(Symbol)
|
|
155
|
+
|
|
156
|
+
field_id = field_mapping[field]
|
|
157
|
+
return field_id if field_id
|
|
158
|
+
|
|
159
|
+
# Raise error for unknown field symbols to prevent silent API errors
|
|
160
|
+
valid_fields = field_mapping.keys.join(", ")
|
|
161
|
+
raise ArgumentError, "Unknown field: #{field.inspect}. Valid fields: #{valid_fields}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Resolve an array of fields (symbols or strings) to field IDs
|
|
165
|
+
def resolve_field_ids(fields)
|
|
166
|
+
fields.map { |field| resolve_field_id(field) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Reverse sort order for last() method
|
|
170
|
+
def reverse_sort_order(sort)
|
|
171
|
+
return nil unless sort
|
|
172
|
+
|
|
173
|
+
flip = ->(dir) { dir.to_sym == :asc ? :desc : :asc }
|
|
174
|
+
|
|
175
|
+
case sort
|
|
176
|
+
when Hash then sort.transform_values(&flip)
|
|
177
|
+
when Array then sort.map { |field, dir| [field, flip.call(dir)] }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airtable
|
|
4
|
+
module ORM
|
|
5
|
+
class Schema
|
|
6
|
+
CACHE_KEY = "airtable:schema"
|
|
7
|
+
CACHE_EXPIRY = 24.hours
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def fetch(base_id, force_refresh: false)
|
|
11
|
+
clear_cache(base_id) if force_refresh
|
|
12
|
+
|
|
13
|
+
cache.fetch(cache_key(base_id), expires_in: CACHE_EXPIRY) { fetch_from_api(base_id) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clear_cache(base_id)
|
|
17
|
+
cache.delete(cache_key(base_id))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def required_table_ids
|
|
23
|
+
ORM.config.table_ids
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fetch_from_api(base_id)
|
|
27
|
+
response = client.connection.get("/v0/meta/bases/#{base_id}/tables")
|
|
28
|
+
parsed_response = response.body
|
|
29
|
+
|
|
30
|
+
if response.success?
|
|
31
|
+
indexed_schema(parsed_response)
|
|
32
|
+
else
|
|
33
|
+
Http::Client.raise_api_error(response.status, parsed_response)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def indexed_schema(parsed_response)
|
|
38
|
+
tables = parsed_response.deep_symbolize_keys[:tables]
|
|
39
|
+
|
|
40
|
+
# Filter only required tables
|
|
41
|
+
tables.select! { |table| required_table_ids.include?(table[:id]) }
|
|
42
|
+
|
|
43
|
+
# Transform fields into hash indexed by field id
|
|
44
|
+
tables.each do |table|
|
|
45
|
+
table[:fields] = table[:fields].index_by { |field| field[:id] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Index tables by table id
|
|
49
|
+
tables.index_by { |table| table[:id] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def cache_key(base_id)
|
|
53
|
+
"#{CACHE_KEY}:#{base_id}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cache
|
|
57
|
+
ORM.config.cache
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def client
|
|
61
|
+
Http::Client.default
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airtable
|
|
4
|
+
module ORM
|
|
5
|
+
# Opt-in RSpec test support: `require "airtable/orm/testing"`. Depends on rspec-mocks at
|
|
6
|
+
# runtime, which is why the directory is ignored by the gem's Zeitwerk loader.
|
|
7
|
+
module Testing
|
|
8
|
+
class << self
|
|
9
|
+
# Absolute path to a JSON dump of /v0/meta/bases/:id/tables — the host points this at
|
|
10
|
+
# its own fixture before using stub_airtable_schema.
|
|
11
|
+
attr_accessor :schema_fixture_path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Include into RSpec (`config.include Airtable::ORM::Testing::StubHelpers, :airtable`)
|
|
15
|
+
# and call stub_airtable_http_client in a before hook — no HTTP leaves the process.
|
|
16
|
+
module StubHelpers
|
|
17
|
+
# Stub schema fetch to use the fixture file instead of the /v0/meta API.
|
|
18
|
+
# Returns the same schema data for all fetch calls, so tests work regardless
|
|
19
|
+
# of which base they're configured for.
|
|
20
|
+
def stub_airtable_schema
|
|
21
|
+
path = Testing.schema_fixture_path
|
|
22
|
+
raise ArgumentError, "Set Airtable::ORM::Testing.schema_fixture_path first" unless path
|
|
23
|
+
|
|
24
|
+
raw_schema = JSON.parse(File.read(path))
|
|
25
|
+
indexed_schema = Airtable::ORM::Schema.send(:indexed_schema, raw_schema)
|
|
26
|
+
|
|
27
|
+
allow(Airtable::ORM::Schema).to receive(:fetch).and_return(indexed_schema)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Route every request through a Faraday test adapter (returned for stubbing) and
|
|
31
|
+
# bypass the API-key check in Http::Client.default with a real client instance so
|
|
32
|
+
# escape/other helpers keep working.
|
|
33
|
+
def stub_airtable_http_client
|
|
34
|
+
@stubs = Faraday::Adapter::Test::Stubs.new
|
|
35
|
+
|
|
36
|
+
stub_connection = Faraday.new do |builder|
|
|
37
|
+
builder.request :json
|
|
38
|
+
builder.response :json
|
|
39
|
+
builder.adapter :test, @stubs
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
fake_client = Airtable::ORM::Http::Client.new("test_api_key")
|
|
43
|
+
allow(fake_client).to receive(:connection).and_return(stub_connection)
|
|
44
|
+
allow(Airtable::ORM::Http::Client).to receive(:default).and_return(fake_client)
|
|
45
|
+
|
|
46
|
+
stub_airtable_schema
|
|
47
|
+
|
|
48
|
+
@stubs
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/airtable/orm.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "active_support"
|
|
6
|
+
# The lib leans broadly on AS core exts standalone (pluck, deep_symbolize_keys, 24.hours,
|
|
7
|
+
# truncate…) — active_model's transitive requires load only a tiny subset.
|
|
8
|
+
require "active_support/core_ext"
|
|
9
|
+
require "faraday"
|
|
10
|
+
require "faraday/net_http_persistent"
|
|
11
|
+
require "logger"
|
|
12
|
+
require "time"
|
|
13
|
+
|
|
14
|
+
module Airtable; end
|
|
15
|
+
|
|
16
|
+
loader = Zeitwerk::Loader.for_gem_extension(Airtable)
|
|
17
|
+
loader.inflector.inflect("orm" => "ORM") # orm.rb / orm/ → ORM, not Orm
|
|
18
|
+
loader.ignore("#{__dir__}/orm/errors.rb") # one file, many constants — required eagerly below
|
|
19
|
+
# RSpec-dependent test support, opt-in via require "airtable/orm/testing".
|
|
20
|
+
loader.ignore("#{__dir__}/orm/testing.rb", "#{__dir__}/orm/testing")
|
|
21
|
+
loader.setup
|
|
22
|
+
|
|
23
|
+
module Airtable
|
|
24
|
+
module ORM
|
|
25
|
+
class << self
|
|
26
|
+
def config
|
|
27
|
+
@config ||= Configuration.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configure
|
|
31
|
+
yield config
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# True when an API key is configured — hosts gate network-touching hooks on this.
|
|
35
|
+
def configured?
|
|
36
|
+
config.api_key.present?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset!
|
|
40
|
+
@config = nil
|
|
41
|
+
Http::Client.reset!
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
require_relative "orm/errors"
|
data/lib/airtable-orm.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: airtable-orm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Roman Sklenar
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activemodel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: faraday
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.9'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.9'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: faraday-net_http_persistent
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.0'
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: zeitwerk
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '2.6'
|
|
67
|
+
type: :runtime
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '2.6'
|
|
74
|
+
description: CRUD, query DSL, associations, schema introspection, batch updates and
|
|
75
|
+
rate limiting for Airtable — behind a branded error hierarchy that never leaks the
|
|
76
|
+
underlying HTTP client.
|
|
77
|
+
email:
|
|
78
|
+
- mail@romansklenar.cz
|
|
79
|
+
executables: []
|
|
80
|
+
extensions: []
|
|
81
|
+
extra_rdoc_files: []
|
|
82
|
+
files:
|
|
83
|
+
- CHANGELOG.md
|
|
84
|
+
- LICENSE.txt
|
|
85
|
+
- README.md
|
|
86
|
+
- lib/airtable-orm.rb
|
|
87
|
+
- lib/airtable/orm.rb
|
|
88
|
+
- lib/airtable/orm/associations.rb
|
|
89
|
+
- lib/airtable/orm/attributes.rb
|
|
90
|
+
- lib/airtable/orm/base.rb
|
|
91
|
+
- lib/airtable/orm/batch_result.rb
|
|
92
|
+
- lib/airtable/orm/collection.rb
|
|
93
|
+
- lib/airtable/orm/configuration.rb
|
|
94
|
+
- lib/airtable/orm/core.rb
|
|
95
|
+
- lib/airtable/orm/errors.rb
|
|
96
|
+
- lib/airtable/orm/http/client.rb
|
|
97
|
+
- lib/airtable/orm/http/error_handler.rb
|
|
98
|
+
- lib/airtable/orm/http/rate_limiter.rb
|
|
99
|
+
- lib/airtable/orm/persistence.rb
|
|
100
|
+
- lib/airtable/orm/querying.rb
|
|
101
|
+
- lib/airtable/orm/schema.rb
|
|
102
|
+
- lib/airtable/orm/testing.rb
|
|
103
|
+
- lib/airtable/orm/testing/stub_helpers.rb
|
|
104
|
+
- lib/airtable/orm/version.rb
|
|
105
|
+
homepage: https://github.com/romansklenar/airtable-orm
|
|
106
|
+
licenses:
|
|
107
|
+
- MIT
|
|
108
|
+
metadata:
|
|
109
|
+
homepage_uri: https://github.com/romansklenar/airtable-orm
|
|
110
|
+
source_code_uri: https://github.com/romansklenar/airtable-orm
|
|
111
|
+
changelog_uri: https://github.com/romansklenar/airtable-orm/blob/main/CHANGELOG.md
|
|
112
|
+
bug_tracker_uri: https://github.com/romansklenar/airtable-orm/issues
|
|
113
|
+
rubygems_mfa_required: 'true'
|
|
114
|
+
rdoc_options: []
|
|
115
|
+
require_paths:
|
|
116
|
+
- lib
|
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ">="
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '3.2'
|
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '0'
|
|
127
|
+
requirements: []
|
|
128
|
+
rubygems_version: 4.0.12
|
|
129
|
+
specification_version: 4
|
|
130
|
+
summary: ActiveModel-style ORM for the Airtable API
|
|
131
|
+
test_files: []
|