airmodel 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|