airmodel 1.0.0 → 1.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 +4 -4
- data/README.md +19 -11
- data/airmodel.gemspec +4 -2
- data/config/bases.yml +4 -4
- data/lib/airmodel.rb +1 -1
- data/lib/airmodel/associable.rb +18 -4
- data/lib/airmodel/model.rb +8 -13
- data/lib/airmodel/query.rb +62 -18
- data/lib/airmodel/version.rb +1 -1
- data/spec/associable_spec.rb +16 -28
- data/spec/model_spec.rb +44 -82
- data/spec/query_spec.rb +69 -23
- data/spec/spec_helper.rb +12 -13
- metadata +36 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86835f77844aa599d172c8cb92e2aac8f13cbedf
|
4
|
+
data.tar.gz: d37a0a44a684f943f68ad7dc5e5ac077ac5752b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9696fc07326864a461978321b419c09cc4755cef0235593b02f76af93a7ebc65f7259d567b2fb4dafb4f38d9e4514e7926258f559dec2c5e34cb022171c385a3
|
7
|
+
data.tar.gz: c91f7ead3cfb6ee25ae107804a3ea7529f4be08126c9f04f757e66b7e3dfb9fa6f449973902d26d1cde6c81a2f6fd55f7d144f2c5fe48f64605a2ae79ad869af
|
data/README.md
CHANGED
@@ -52,7 +52,9 @@ Now you can write code like
|
|
52
52
|
|
53
53
|
Song.all
|
54
54
|
|
55
|
-
Song.where("Artist Name"
|
55
|
+
Song.where("Artist Name" => "The Beatles", "Composer" => "Harrison")
|
56
|
+
|
57
|
+
Song.search(:q => "Let it Be", fields: ["Name", "Album"])
|
56
58
|
|
57
59
|
Song.first
|
58
60
|
|
@@ -60,29 +62,35 @@ Now you can write code like
|
|
60
62
|
|
61
63
|
Song.find("recXYZ")
|
62
64
|
|
63
|
-
Song.
|
65
|
+
Song.find_by("Composer" => "Harrison")
|
64
66
|
|
65
67
|
|
66
|
-
|
68
|
+
Queries are chainable, e.g.
|
67
69
|
|
68
|
-
Song.where("rating" => 5).where('
|
70
|
+
Song.where("rating" => 5).where('Artist' => "Fiona Apple").order("rating", "DESC").limit(5)
|
69
71
|
|
70
|
-
There's also a
|
71
|
-
supplied in your `Model.where()` statements, and replaces them with a raw
|
72
|
+
There's also a `Model.by_formula` query, which lets you pass explicit
|
72
73
|
[Airtable
|
73
|
-
|
74
|
+
Formulas](https://support.airtable.com/hc/en-us/articles/203255215-Formula-field-reference)
|
74
75
|
|
75
|
-
You can
|
76
|
+
You can chain `.limit` and `.order` with a `.by_forumla` query.
|
76
77
|
|
77
78
|
Song.by_formula("NOT({Rating} < 3)").order("rating", "DESC").limit(5)
|
78
79
|
|
79
|
-
See lib/airmodel/model.rb for all
|
80
|
+
See `lib/airmodel/model.rb` for all model methods, and
|
81
|
+
`lib/airmodel/query.rb` for all Query methods.
|
80
82
|
|
81
83
|
|
82
84
|
Contributions
|
83
85
|
----------------
|
84
86
|
|
85
|
-
|
86
|
-
|
87
|
+
I'm currently testing against a live Airtable base, because stubbing API
|
88
|
+
calls has occasionally yielded false positives in my specs. To run the tests,
|
89
|
+
create an Airtable account, set `AIRTABLE_API_KEY=[your API key]` in your `.env` file, and
|
90
|
+
then visit [this link](https://airtable.com/invite/l?inviteId=invj96HyFOB6GF8Vq&inviteToken=2e98eff03a646162344bb997a06645e3)
|
91
|
+
to request access to the base.
|
92
|
+
|
93
|
+
Once that's all set, write a passing test for your feature and send a pull request.
|
87
94
|
|
95
|
+
Thanks!
|
88
96
|
|
data/airmodel.gemspec
CHANGED
@@ -20,8 +20,10 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.add_development_dependency 'rake', '~> 10'
|
21
21
|
spec.add_development_dependency 'rspec', '~> 3'
|
22
22
|
spec.add_development_dependency 'pry', '~> 0.10'
|
23
|
-
spec.add_development_dependency '
|
23
|
+
spec.add_development_dependency 'vcr', '~> 3.0'
|
24
|
+
spec.add_development_dependency 'dotenv', '~> 2.1'
|
25
|
+
spec.add_development_dependency 'webmock', '~> 2.3'
|
24
26
|
|
25
27
|
spec.add_dependency 'airtable', '~> 0.0.9'
|
26
|
-
spec.add_dependency 'activesupport', '~>
|
28
|
+
spec.add_dependency 'activesupport', '~> 4.0'
|
27
29
|
end
|
data/config/bases.yml
CHANGED
data/lib/airmodel.rb
CHANGED
data/lib/airmodel/associable.rb
CHANGED
@@ -15,10 +15,15 @@ module Airmodel
|
|
15
15
|
base_id: self.send(args[:base_key]),
|
16
16
|
table_name: args[:table_name] || association_name.to_s.tableize
|
17
17
|
}
|
18
|
+
# maybe the base is defined in the config file
|
19
|
+
elsif c = Airmodel.bases[args[:class_name].tableize.to_sym]
|
20
|
+
c
|
21
|
+
# maybe the base is just a table in the same base as the parent
|
18
22
|
else
|
19
|
-
|
20
|
-
|
21
|
-
|
23
|
+
{
|
24
|
+
base_id: self.class.base_config[:base_id],
|
25
|
+
table_name: args[:table_name] || association_name.to_s.tableize
|
26
|
+
}
|
22
27
|
end
|
23
28
|
finder_name = "@#{association_name}_finder"
|
24
29
|
if f = instance_variable_get(finder_name)
|
@@ -28,10 +33,19 @@ module Airmodel
|
|
28
33
|
@base_id = config[:base_id]
|
29
34
|
@table_name = config[:table_name]
|
30
35
|
end
|
31
|
-
|
36
|
+
constraints = if args[:constraints].respond_to?(:call)
|
37
|
+
args[:constraints].call(self)
|
38
|
+
else
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
instance_variable_set(finder_name, finder.where(constraints))
|
32
42
|
end
|
33
43
|
end
|
34
44
|
end
|
35
45
|
|
46
|
+
def default_has_many_contraints
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
36
50
|
end
|
37
51
|
end
|
data/lib/airmodel/model.rb
CHANGED
@@ -15,10 +15,16 @@ module Airmodel
|
|
15
15
|
Query.new(self).by_formula(args)
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.search(args)
|
19
|
+
Query.new(self).search(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Model.order("Name DESC")
|
18
23
|
def self.order(args)
|
19
|
-
Query.new(self).order(args)
|
24
|
+
Query.new(self).order(*args)
|
20
25
|
end
|
21
26
|
|
27
|
+
# Model.limit(10)
|
22
28
|
def self.limit(args)
|
23
29
|
Query.new(self).limit(args)
|
24
30
|
end
|
@@ -45,18 +51,7 @@ module Airmodel
|
|
45
51
|
|
46
52
|
# find a record by specified attributes, return it
|
47
53
|
def self.find_by(filters)
|
48
|
-
|
49
|
-
results = self.classify table.find(filters[:id])
|
50
|
-
else
|
51
|
-
formula = "AND(" + filters.map{|k,v| "{#{k}}='#{v}'" }.join(',') + ")"
|
52
|
-
results = self.classify(
|
53
|
-
table.records(
|
54
|
-
filterByFormula: formula,
|
55
|
-
limit: 1
|
56
|
-
)
|
57
|
-
)
|
58
|
-
end
|
59
|
-
results.count == 0 ? nil : results.first
|
54
|
+
Query.new(self).find_by(filters)
|
60
55
|
end
|
61
56
|
|
62
57
|
# default to whatever order Airtable returns
|
data/lib/airmodel/query.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Airmodel
|
2
2
|
class Query
|
3
|
+
include Enumerable
|
3
4
|
|
4
5
|
def initialize(querying_class)
|
5
6
|
@querying_class = querying_class
|
@@ -8,7 +9,9 @@ module Airmodel
|
|
8
9
|
def params
|
9
10
|
@params ||= {
|
10
11
|
where_clauses: {},
|
11
|
-
|
12
|
+
formulas: [],
|
13
|
+
order: @querying_class.default_sort,
|
14
|
+
offset: nil
|
12
15
|
}
|
13
16
|
end
|
14
17
|
|
@@ -18,45 +21,76 @@ module Airmodel
|
|
18
21
|
end
|
19
22
|
|
20
23
|
def by_formula(formula)
|
21
|
-
params[:
|
22
|
-
|
24
|
+
params[:formulas].push formula
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def search(args={})
|
29
|
+
if args && args[:q] && args[:fields]
|
30
|
+
searchfields = if args[:fields].is_a?(String)
|
31
|
+
args[:fields].split(",").map{|f| f.strip }
|
32
|
+
else
|
33
|
+
args[:fields]
|
34
|
+
end
|
35
|
+
query = if args[:q].respond_to?(:downcase)
|
36
|
+
args[:q].downcase
|
37
|
+
else
|
38
|
+
args[:q]
|
39
|
+
end
|
40
|
+
f = "OR(" + searchfields.map{|field|
|
41
|
+
# convert strings to case-insensitive searches
|
42
|
+
"FIND('#{query}', LOWER({#{field}}))"
|
43
|
+
}.join(',') + ")"
|
44
|
+
params[:formulas].push f
|
45
|
+
end
|
23
46
|
self
|
24
47
|
end
|
25
48
|
|
26
49
|
def limit(lim)
|
27
|
-
params[:limit] = lim
|
50
|
+
params[:limit] = lim ? lim.to_i : nil
|
28
51
|
self
|
29
52
|
end
|
30
53
|
|
31
|
-
def order(
|
32
|
-
|
54
|
+
def order(order_string)
|
55
|
+
if order_string
|
56
|
+
ordr = order_string.split(" ")
|
57
|
+
column = ordr.first
|
58
|
+
direction = ordr.length > 1 ? ordr.last.downcase : "asc"
|
59
|
+
params[:order] = [column, direction]
|
60
|
+
end
|
33
61
|
self
|
34
62
|
end
|
35
63
|
|
64
|
+
|
65
|
+
def offset(airtable_offset_key)
|
66
|
+
params[:offset] = airtable_offset_key
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# return saved airtable offset for this query
|
71
|
+
def get_offset
|
72
|
+
@offset
|
73
|
+
end
|
74
|
+
|
36
75
|
# add kicker methods
|
37
76
|
def to_a
|
38
77
|
puts "RUNNING EXPENSIVE API QUERY TO AIRTABLE (#{@querying_class.name})"
|
39
|
-
#
|
40
|
-
formula =
|
41
|
-
|
78
|
+
# merge explicit formulas and abstracted where-clauses into one Airtable Formula
|
79
|
+
formula = "AND(" + params[:where_clauses].map{|k,v| "{#{k}}='#{v}'" }.join(',') + params[:formulas].join(',') + ")"
|
80
|
+
records = @querying_class.table.records(
|
42
81
|
sort: params[:order],
|
43
82
|
filterByFormula: formula,
|
44
|
-
limit: params[:limit]
|
83
|
+
limit: params[:limit],
|
84
|
+
offset: params[:offset]
|
45
85
|
)
|
86
|
+
@offset = records.offset
|
87
|
+
@querying_class.classify records
|
46
88
|
end
|
47
89
|
|
48
90
|
def all
|
49
91
|
to_a
|
50
92
|
end
|
51
93
|
|
52
|
-
def first
|
53
|
-
to_a.first
|
54
|
-
end
|
55
|
-
|
56
|
-
def last
|
57
|
-
to_a.last
|
58
|
-
end
|
59
|
-
|
60
94
|
def each(&block)
|
61
95
|
to_a.each(&block)
|
62
96
|
end
|
@@ -69,6 +103,16 @@ module Airmodel
|
|
69
103
|
to_a.inspect
|
70
104
|
end
|
71
105
|
|
106
|
+
def last
|
107
|
+
to_a.last
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_by(filters)
|
111
|
+
params[:limit] = 1
|
112
|
+
params[:where_clauses] = filters
|
113
|
+
first
|
114
|
+
end
|
115
|
+
|
72
116
|
end
|
73
117
|
end
|
74
118
|
|
data/lib/airmodel/version.rb
CHANGED
data/spec/associable_spec.rb
CHANGED
@@ -3,45 +3,37 @@ require 'spec_helper'
|
|
3
3
|
class Song < Airmodel::Model
|
4
4
|
end
|
5
5
|
|
6
|
+
class Album < Airmodel::Model
|
7
|
+
end
|
8
|
+
|
6
9
|
class ParentModel < Airmodel::Model
|
7
10
|
has_many :songs
|
8
11
|
|
9
12
|
def dynamically_assigned_child_base_id
|
10
|
-
|
13
|
+
"appTE8VIb595FI4c6"
|
11
14
|
end
|
15
|
+
|
12
16
|
end
|
13
17
|
|
14
18
|
describe ParentModel do
|
15
19
|
|
16
|
-
before(:each) do
|
17
|
-
config = Airmodel.bases[:albums]
|
18
|
-
#stub INDEX requests
|
19
|
-
stub_airtable_response!(
|
20
|
-
Regexp.new("https://api.airtable.com/v0/#{config[:base_id]}/#{config[:table_name]}"),
|
21
|
-
{ "records" => [{"id": "recXYZ", fields: {"color":"red"} }, {"id":"recABC", fields: {"color": "blue"} }] }
|
22
|
-
)
|
23
|
-
#stub CREATE requests
|
24
|
-
stub_airtable_response!("https://api.airtable.com/v0/appXYZ/albums",
|
25
|
-
{ "fields" => { "color" => "red", "foo" => "bar" }, "id" => "12345" },
|
26
|
-
:post
|
27
|
-
)
|
28
|
-
end
|
29
|
-
|
30
|
-
after(:each) do
|
31
|
-
FakeWeb.clean_registry
|
32
|
-
end
|
33
|
-
|
34
|
-
|
35
20
|
describe 'has_many' do
|
36
21
|
it 'should return a list of songs' do
|
37
22
|
songs = ParentModel.new.songs
|
38
|
-
expect(songs.
|
23
|
+
expect(songs.first.is_a? Song).to be true
|
24
|
+
end
|
25
|
+
|
26
|
+
it "Should look in the parent model's base when not passed a base_key" do
|
27
|
+
Song.has_many :albums
|
28
|
+
albums = Song.first.albums.all
|
29
|
+
expect(albums.first.name).to eq "Blood on the Tracks"
|
39
30
|
end
|
40
31
|
|
41
32
|
it 'should raise NoSuchBase when passed a weird association not backed by bases.yml' do
|
42
33
|
begin
|
43
34
|
ParentModel.has_many :unusual_children
|
44
|
-
|
35
|
+
ParentModel.new.unusual_children
|
36
|
+
expect(true).to eq false
|
45
37
|
rescue Airmodel::NoSuchBase
|
46
38
|
true
|
47
39
|
end
|
@@ -52,13 +44,9 @@ describe ParentModel do
|
|
52
44
|
end
|
53
45
|
|
54
46
|
it 'should work with a base_key instead of a yml file' do
|
55
|
-
|
56
|
-
Regexp.new("https://api.airtable.com/v0/appABCDEF/tunes"),
|
57
|
-
{ "records" => [{"id": "recXYZ", fields: {"color":"red"} }, {"id":"recABC", fields: {"color": "blue"} }] }
|
58
|
-
)
|
59
|
-
ParentModel.has_many :tunes, base_key: 'dynamically_assigned_child_base_id', class_name: 'Song'
|
47
|
+
ParentModel.has_many :tunes, base_key: 'dynamically_assigned_child_base_id', class_name: 'Song', table_name: "Songs"
|
60
48
|
tunes = ParentModel.new.tunes
|
61
|
-
expect(tunes.first.table.worksheet_name).to eq
|
49
|
+
expect(tunes.first.table.worksheet_name).to eq "Songs"
|
62
50
|
end
|
63
51
|
|
64
52
|
it 'should let me define the important args however I like'
|
data/spec/model_spec.rb
CHANGED
@@ -5,24 +5,6 @@ end
|
|
5
5
|
|
6
6
|
describe Album do
|
7
7
|
|
8
|
-
before(:each) do
|
9
|
-
config = Airmodel.bases[:albums]
|
10
|
-
#stub INDEX requests
|
11
|
-
stub_airtable_response!(
|
12
|
-
Regexp.new("https://api.airtable.com/v0/#{config[:base_id]}/#{config[:table_name]}"),
|
13
|
-
{ "records" => [{"id": "recXYZ", fields: {"color":"red"} }, {"id":"recABC", fields: {"color": "blue"} }] }
|
14
|
-
)
|
15
|
-
#stub CREATE requests
|
16
|
-
stub_airtable_response!("https://api.airtable.com/v0/appXYZ/albums",
|
17
|
-
{ "fields" => { "color" => "red", "foo" => "bar" }, "id" => "12345" },
|
18
|
-
:post
|
19
|
-
)
|
20
|
-
end
|
21
|
-
|
22
|
-
after(:each) do
|
23
|
-
FakeWeb.clean_registry
|
24
|
-
end
|
25
|
-
|
26
8
|
describe "Class Methods" do
|
27
9
|
describe "table" do
|
28
10
|
it "should return an Airtable::Table object" do
|
@@ -48,53 +30,47 @@ describe Album do
|
|
48
30
|
end
|
49
31
|
|
50
32
|
describe "all" do
|
33
|
+
use_vcr_cassette
|
51
34
|
it "should return a list of airtable records" do
|
52
35
|
records = Album.all
|
53
|
-
expect(records.first.id).to eq "
|
36
|
+
expect(records.first.id).to eq "rec0bTuIoUQVPMsmi"
|
54
37
|
end
|
55
38
|
end
|
56
39
|
|
57
40
|
describe "where" do
|
41
|
+
use_vcr_cassette
|
58
42
|
it "should return a list of airtable records that match filters" do
|
59
|
-
records = Album.where(
|
43
|
+
records = Album.where(name: "Blood on the Tracks")
|
60
44
|
expect(records.first.class).to eq Album
|
61
|
-
expect(records.first.
|
45
|
+
expect(records.first.name).to eq "Blood on the Tracks"
|
62
46
|
end
|
63
47
|
end
|
64
48
|
|
65
49
|
describe "find" do
|
50
|
+
use_vcr_cassette
|
66
51
|
it "should call airtable-ruby's 'find' method when passed just one record ID" do
|
67
|
-
|
68
|
-
record
|
69
|
-
expect(record.name).to eq "example record"
|
52
|
+
record = Album.find("rec52DfV4E2I2kzrS")
|
53
|
+
expect(record.name).to eq "Voodoo"
|
70
54
|
end
|
71
55
|
it "should return an ordered list of records when passed an array of record IDs" do
|
72
|
-
records = Album.find(["
|
56
|
+
records = Album.find(["rec52DfV4E2I2kzrS", "rec0bTuIoUQVPMsmi"])
|
73
57
|
expect(records.class).to eq Array
|
74
|
-
expect(records.first.id).to eq "
|
58
|
+
expect(records.first.id).to eq "rec52DfV4E2I2kzrS"
|
75
59
|
expect(records.first.class).to eq Album
|
76
60
|
end
|
77
61
|
end
|
78
62
|
|
79
63
|
describe "find_by" do
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
)
|
85
|
-
record = Album.find_by(color: "blue")
|
86
|
-
expect(record.color).to eq "blue"
|
87
|
-
expect(record.class).to eq Album
|
88
|
-
end
|
89
|
-
it "should call airtable-ruby's 'find' method when the filter is an id" do
|
90
|
-
stub_airtable_response! "https://api.airtable.com/v0/appXYZ/albums/recABC", { "id":"recABC", fields: {"name": "example record"} }
|
91
|
-
record = Album.find_by(id: "recABC")
|
92
|
-
expect(record.name).to eq "example record"
|
64
|
+
use_vcr_cassette
|
65
|
+
it "should return one record that matches the supplied filters" do
|
66
|
+
record = Album.find_by(name: "Voodoo")
|
67
|
+
expect(record.name).to eq "Voodoo"
|
93
68
|
expect(record.class).to eq Album
|
94
69
|
end
|
95
70
|
end
|
96
71
|
|
97
72
|
describe "first" do
|
73
|
+
use_vcr_cassette
|
98
74
|
it "should return the first record" do
|
99
75
|
record = Album.first
|
100
76
|
expect(record.class).to eq Album
|
@@ -102,21 +78,22 @@ describe Album do
|
|
102
78
|
end
|
103
79
|
|
104
80
|
describe "create" do
|
81
|
+
use_vcr_cassette
|
105
82
|
it "should create a new record" do
|
106
|
-
record = Album.create(
|
107
|
-
expect(record.id).
|
83
|
+
record = Album.create("Name" => "Abbey Road")
|
84
|
+
expect(record.id).not_to eq nil
|
85
|
+
record.destroy
|
108
86
|
end
|
109
87
|
end
|
110
88
|
|
111
89
|
describe "patch" do
|
90
|
+
use_vcr_cassette
|
112
91
|
it "should update a record" do
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
)
|
117
|
-
record
|
118
|
-
record = Album.patch("12345", { color: "blue" })
|
119
|
-
expect(record.color).to eq "blue"
|
92
|
+
record = Album.create("Name" => "Let It Be")
|
93
|
+
notes = "It wasn't really their last one"
|
94
|
+
record = Album.patch(record.id, { "Notes" => notes })
|
95
|
+
expect(record.notes).to eq notes
|
96
|
+
record.destroy
|
120
97
|
end
|
121
98
|
end
|
122
99
|
|
@@ -125,71 +102,56 @@ describe Album do
|
|
125
102
|
describe "Instance Methods" do
|
126
103
|
|
127
104
|
describe "save" do
|
128
|
-
record = Album.new(color: "red")
|
129
105
|
it "should create a new record" do
|
106
|
+
record = Album.new("Name" => "His California Record")
|
130
107
|
record.save
|
131
|
-
expect(record.id).
|
108
|
+
expect(record.id).not_to be nil
|
109
|
+
record.destroy
|
132
110
|
end
|
133
111
|
it "should update an existing record" do
|
134
|
-
|
135
|
-
|
136
|
-
:get
|
137
|
-
)
|
138
|
-
stub_airtable_response!("https://api.airtable.com/v0/appXYZ/albums/12345",
|
139
|
-
{ "fields" => { "color" => "blue" }, "id" => "12345" },
|
140
|
-
:patch
|
141
|
-
)
|
142
|
-
record[:color] = "blue"
|
112
|
+
record = Album.create("Name" => "His California Record")
|
113
|
+
record["Artist"] = "Bobby Bland"
|
143
114
|
record.save
|
144
|
-
expect(record.
|
115
|
+
expect(record.artist).to eq "Bobby Bland"
|
116
|
+
record.destroy
|
145
117
|
end
|
146
118
|
end
|
147
119
|
|
148
120
|
describe "destroy" do
|
149
121
|
it "should delete a record" do
|
150
|
-
|
151
|
-
{ "deleted": true, "id" => "12345" },
|
152
|
-
:delete
|
153
|
-
)
|
154
|
-
response = Album.new(id: "12345").destroy
|
122
|
+
response = Album.create("Name" => "12345").destroy
|
155
123
|
expect(response["deleted"]).to eq true
|
156
124
|
end
|
157
125
|
end
|
158
126
|
|
159
127
|
describe "update" do
|
160
128
|
it "should update the supplied attrs on an existing record" do
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
record = Album.create(color: "red", id:"12345")
|
166
|
-
record.update(color: "green")
|
167
|
-
expect(record.color).to eq "green"
|
129
|
+
record = Album.create("Name" => "Blue")
|
130
|
+
record.update("Name" => "Green")
|
131
|
+
expect(record.name).to eq "Green"
|
132
|
+
record.destroy
|
168
133
|
end
|
169
134
|
end
|
170
135
|
|
171
136
|
describe "cache_key" do
|
172
137
|
it "should return a unique key that can be used to id this record in memcached" do
|
173
|
-
record = Album.
|
174
|
-
expect(record.cache_key).to eq "
|
138
|
+
record = Album.first
|
139
|
+
expect(record.cache_key).to eq "albums_#{record.id}"
|
175
140
|
end
|
176
141
|
end
|
177
142
|
|
178
143
|
describe "changed_fields" do
|
179
144
|
it "should return a hash of attrs changed since last save" do
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
record = Album.create(color: 'red')
|
185
|
-
record[:color] = 'green'
|
186
|
-
expect(record.changed_fields).to have_key 'color'
|
145
|
+
record = Album.create("Name" => "Pinkerton")
|
146
|
+
record["Name"] = "Green"
|
147
|
+
expect(record.changed_fields).to have_key "Name"
|
148
|
+
record.destroy
|
187
149
|
end
|
188
150
|
end
|
189
151
|
|
190
152
|
describe "new_record?" do
|
191
153
|
it "should return true if the record hasn't been saved to airtable yet" do
|
192
|
-
record = Album.new(
|
154
|
+
record = Album.new("Name" => "Jim!")
|
193
155
|
expect(record.new_record?).to eq true
|
194
156
|
end
|
195
157
|
end
|
data/spec/query_spec.rb
CHANGED
@@ -5,41 +5,87 @@ end
|
|
5
5
|
|
6
6
|
describe Album do
|
7
7
|
|
8
|
-
before(:each) do
|
9
|
-
config = Airmodel.bases[:albums]
|
10
|
-
#stub INDEX requests
|
11
|
-
stub_airtable_response!(
|
12
|
-
Regexp.new("https://api.airtable.com/v0/#{config[:base_id]}/#{config[:table_name]}"),
|
13
|
-
{ "records" => [{"id": "recXYZ", fields: {"color":"red"} }, {"id":"recABC", fields: {"color": "blue"} }] }
|
14
|
-
)
|
15
|
-
end
|
16
|
-
|
17
|
-
after(:each) do
|
18
|
-
FakeWeb.clean_registry
|
19
|
-
end
|
20
|
-
|
21
|
-
|
22
8
|
describe "where" do
|
23
9
|
it "allows chaining" do
|
24
10
|
q = Album.where(
|
25
11
|
"name" => "Tidal",
|
26
|
-
"artist" => "Fiona Apple"
|
27
|
-
|
28
|
-
).limit(10)
|
12
|
+
"artist" => "Fiona Apple"
|
13
|
+
).limit(10).order("Name DESC")
|
29
14
|
expect(q.class).to eq Airmodel::Query
|
30
15
|
# it should execute the query on
|
31
16
|
# query.all, and return an array
|
32
17
|
expect(q.all.class).to be Array
|
18
|
+
expect(q.count).to eq 1
|
33
19
|
end
|
34
20
|
|
35
|
-
it "can
|
36
|
-
formula = "NOT({Rating}
|
37
|
-
q = Album.
|
38
|
-
expect(q.params[:
|
39
|
-
expect(q.params[:formula]).to be formula
|
21
|
+
it "can use a raw airtable formula" do
|
22
|
+
formula = "NOT({Rating} > 3)"
|
23
|
+
q = Album.by_formula(formula)
|
24
|
+
expect(q.params[:formulas].to_s).to eq [formula].to_s
|
40
25
|
expect(q.all.class).to be Array
|
26
|
+
expect(q.count).to eq 1
|
41
27
|
end
|
42
28
|
end
|
43
29
|
|
44
|
-
|
30
|
+
describe "limit" do
|
31
|
+
it "limits the results to the specified number" do
|
32
|
+
q = Album.limit(2)
|
33
|
+
expect(q.count).to eq 2
|
34
|
+
end
|
35
|
+
|
36
|
+
it "accepts a string instead of an integer" do
|
37
|
+
q = Album.limit("1")
|
38
|
+
expect(q.count).to eq 1
|
39
|
+
end
|
40
|
+
|
41
|
+
it "accepts nil as an all-pass filter" do
|
42
|
+
q = Album.limit(nil)
|
43
|
+
expect(q.count).to eq Album.all.count
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "search" do
|
48
|
+
it "searches in args[:fields] for args[:value]" do
|
49
|
+
q = Album.search(q: "Fiona", fields: ["Artist"])
|
50
|
+
expect(q.count).to eq 1
|
51
|
+
expect(q.first.artist).to eq "Fiona Apple"
|
52
|
+
end
|
45
53
|
|
54
|
+
it "can search across multiple fields" do
|
55
|
+
q = Album.search(q: "Sinatra", fields: ["Artist", "Name"])
|
56
|
+
expect(q.count).to eq 2
|
57
|
+
expect(q.first.artist).to eq "Frank Sinatra"
|
58
|
+
expect(q.last.artist).to eq "Dylan"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "can accept field names as a string" do
|
62
|
+
q = Album.search(q: "Sinatra", fields: "Artist, Name")
|
63
|
+
expect(q.count).to eq 2
|
64
|
+
expect(q.first.artist).to eq "Frank Sinatra"
|
65
|
+
expect(q.last.artist).to eq "Dylan"
|
66
|
+
end
|
67
|
+
|
68
|
+
it "is not case-sensitive w/r/t queries" do
|
69
|
+
q = Album.search(q: "dylan", fields: "Artist")
|
70
|
+
expect(q.count).to eq 2
|
71
|
+
end
|
72
|
+
|
73
|
+
it "accepts nil as an all-pass filter" do
|
74
|
+
q = Album.search(nil)
|
75
|
+
expect(q.count).to eq Album.all.count
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "order" do
|
81
|
+
it "can order when passed an array" do
|
82
|
+
q = Album.order("Name ASC")
|
83
|
+
expect(q.first.name).to eq "A Swingin' Affair"
|
84
|
+
q = Album.order("Name")
|
85
|
+
expect(q.first.name).to eq "A Swingin' Affair"
|
86
|
+
q = Album.order("Name DESC")
|
87
|
+
expect(q.first.name).to eq "Voodoo"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,21 +1,20 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
|
5
|
-
|
6
|
-
FakeWeb.register_uri(
|
7
|
-
method,
|
8
|
-
url,
|
9
|
-
body: response.to_json,
|
10
|
-
content_type: "application/json"
|
11
|
-
)
|
12
|
-
end
|
1
|
+
require "airmodel"
|
2
|
+
require "pry"
|
3
|
+
require "vcr"
|
4
|
+
require "dotenv"
|
5
|
+
Dotenv.load
|
13
6
|
|
14
7
|
RSpec.configure do |config|
|
15
8
|
config.color = true
|
9
|
+
config.extend VCR::RSpec::Macros
|
10
|
+
config.order = :random
|
16
11
|
end
|
17
12
|
|
18
|
-
|
13
|
+
VCR.configure do |config| config.allow_http_connections_when_no_cassette = true
|
14
|
+
config.cassette_library_dir = "#{Airmodel.root}/spec/fixtures/vcr_cassettes"
|
15
|
+
config.default_cassette_options = { :record => :new_episodes }
|
16
|
+
config.filter_sensitive_data("<AIRTABLE_API_KEY>") { ENV.fetch('AIRTABLE_API_KEY') }
|
17
|
+
end
|
19
18
|
|
20
19
|
# enable Debug mode in Airtable
|
21
20
|
module Airtable
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: airmodel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- chrisfrankdotfm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-12-
|
11
|
+
date: 2016-12-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -67,19 +67,47 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0.10'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: vcr
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '3.0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dotenv
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: webmock
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.3'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.3'
|
83
111
|
- !ruby/object:Gem::Dependency
|
84
112
|
name: airtable
|
85
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,14 +128,14 @@ dependencies:
|
|
100
128
|
requirements:
|
101
129
|
- - "~>"
|
102
130
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
131
|
+
version: '4.0'
|
104
132
|
type: :runtime
|
105
133
|
prerelease: false
|
106
134
|
version_requirements: !ruby/object:Gem::Requirement
|
107
135
|
requirements:
|
108
136
|
- - "~>"
|
109
137
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
138
|
+
version: '4.0'
|
111
139
|
description: Airtable data in ActiveRecord-style syntax
|
112
140
|
email:
|
113
141
|
- chris.frank@thefutureproject.org
|
@@ -152,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
180
|
version: '0'
|
153
181
|
requirements: []
|
154
182
|
rubyforge_project:
|
155
|
-
rubygems_version: 2.5.
|
183
|
+
rubygems_version: 2.5.2
|
156
184
|
signing_key:
|
157
185
|
specification_version: 4
|
158
186
|
summary: Interact with your Airtable data using ActiveRecord-style models
|