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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 13bc3270f4a9eee17e8559404cdb9f0e9917f899
4
- data.tar.gz: 8ddb417cbac7e452de3f7ca469007370c2a7f6e6
3
+ metadata.gz: 86835f77844aa599d172c8cb92e2aac8f13cbedf
4
+ data.tar.gz: d37a0a44a684f943f68ad7dc5e5ac077ac5752b9
5
5
  SHA512:
6
- metadata.gz: 38013c380e9a5b226c1ab4fe3c80ce10f5901052e9962db2f480fe37c555e29d956accd00b832e83e5aa7fcc5545b2995833ee3a61c0dfa5e0d37775f9cf76ce
7
- data.tar.gz: 3686e3416162fd2744cceb2f81ea0c0272ab4fa6d16009205e1f9d74e34308ef6011a7c6b72dbd558b1e4cd10a67a719b530665c20a1b18fee0765e377ac2a6a
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": "The Beatles", "Composer": "Harrison")
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.find(["recXYZ", "recABC", "recJKL"])
65
+ Song.find_by("Composer" => "Harrison")
64
66
 
65
67
 
66
- Most queries are chainable, e.g.
68
+ Queries are chainable, e.g.
67
69
 
68
- Song.where("rating" => 5).where('artist' => "Fiona Apple").order("rating", "DESC").limit(5)
70
+ Song.where("rating" => 5).where('Artist' => "Fiona Apple").order("rating", "DESC").limit(5)
69
71
 
70
- There's also a special `Model.by_formula` query, which overrides any filters
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
- Formula](https://support.airtable.com/hc/en-us/articles/203255215-Formula-field-reference)
74
+ Formulas](https://support.airtable.com/hc/en-us/articles/203255215-Formula-field-reference)
74
75
 
75
- You can still chain `.limit` and `.order` with a `.by_forumla` query.
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 available methods.
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
- Add a passing test to spec/model_spec.rb, then send a pull
86
- request. Thanks!
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
 
@@ -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 'fakeweb', '~> 1.3'
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', '~> 5.0'
28
+ spec.add_dependency 'activesupport', '~> 4.0'
27
29
  end
@@ -1,6 +1,6 @@
1
1
  :albums:
2
- :table_name: "albums"
3
- :base_id: appXYZ
2
+ :table_name: Albums
3
+ :base_id: appTE8VIb595FI4c6
4
4
  :songs:
5
- :table_name: "songs"
6
- :base_id: appXYZ
5
+ :table_name: Songs
6
+ :base_id: appTE8VIb595FI4c6
@@ -14,7 +14,7 @@ module Airmodel
14
14
  File.expand_path '../..', __FILE__
15
15
  end
16
16
 
17
- def self.client(api_key=ENV["AIRTABLE_API_KEY"])
17
+ def self.client(api_key=ENV.fetch("AIRTABLE_API_KEY") )
18
18
  @@api_client ||= Airtable::Client.new(api_key)
19
19
  @@api_client
20
20
  end
@@ -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
- # the airtable base info is defined in the
20
- # YML config file, with the rest of the data
21
- Airmodel.bases[args[:class_name].tableize.to_sym] || raise(NoSuchBase.new("Couldn't find base '#{association_name}' in config file.\nPlease pass :base_key => foo with your has_many call,\nor add '#{association_name}' to your config file."))
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
- instance_variable_set(finder_name, finder)
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
@@ -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
- if filters[:id]
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
@@ -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
- order: @querying_class.default_sort
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[:where_clauses] = {}
22
- params[:formula] = formula
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(column, direction)
32
- params[:order] = [column, direction.downcase.to_sym]
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
- # filter by explicit formula, or by joining all where_clasues together
40
- formula = params[:formula] || "AND(" + params[:where_clauses].map{|k,v| "{#{k}}='#{v}'" }.join(',') + ")"
41
- @querying_class.classify @querying_class.table.all(
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
 
@@ -1,3 +1,3 @@
1
1
  module Airmodel
2
- VERSION = '1.0.0'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -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
- 'appABCDEF'
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.table_name).to eq '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
- false
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
- stub_airtable_response!(
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 'tunes'
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'
@@ -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 "recXYZ"
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(color: "red")
43
+ records = Album.where(name: "Blood on the Tracks")
60
44
  expect(records.first.class).to eq Album
61
- expect(records.first.color).to eq "red"
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
- stub_airtable_response! "https://api.airtable.com/v0/appXYZ/albums/recABC", { "id":"recABC", fields: {"name": "example record"} }
68
- record = Album.find("recABC")
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(["recABC", "recXYZ"])
56
+ records = Album.find(["rec52DfV4E2I2kzrS", "rec0bTuIoUQVPMsmi"])
73
57
  expect(records.class).to eq Array
74
- expect(records.first.id).to eq "recABC"
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
- it "should return one record that matches the supplied filters", skip_before: true do
81
- stub_airtable_response!(
82
- Regexp.new("https://api.airtable.com/v0/appXYZ/albums"),
83
- { "records" => [{"id":"recABC", fields: {"color": "blue"} }] }
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(color: "red")
107
- expect(record.id).to eq "12345"
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
- stub_airtable_response!(Regexp.new("/v0/appXYZ/albums/12345"),
114
- { "fields" => { "color" => "blue", "foo" => "bar" }, "id" => "12345" },
115
- :patch
116
- )
117
- record = Album.create(color: "red")
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).to eq "12345"
108
+ expect(record.id).not_to be nil
109
+ record.destroy
132
110
  end
133
111
  it "should update an existing record" do
134
- stub_airtable_response!("https://api.airtable.com/v0/appXYZ/albums/12345",
135
- { "fields" => { "color" => "red", "foo" => "bar" }, "id" => "12345" },
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.color).to eq "blue"
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
- stub_airtable_response!("https://api.airtable.com/v0/appXYZ/albums/12345",
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
- stub_airtable_response!("https://api.airtable.com/v0/appXYZ/albums/12345",
162
- { "fields" => { "color" => "green"}, "id" => "12345" },
163
- :patch
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.new(id: "recZXY")
174
- expect(record.cache_key).to eq "albums_recZXY"
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
- stub_airtable_response!("https://api.airtable.com/v0/appXYZ/albums/12345",
181
- { fields: { 'color': 'red' }, "id" => "12345" },
182
- :get
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(color: 'red')
154
+ record = Album.new("Name" => "Jim!")
193
155
  expect(record.new_record?).to eq true
194
156
  end
195
157
  end
@@ -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
- "great" => true
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 replace where_clauses with a raw airtable formula" do
36
- formula = "NOT({Rating} < 4"
37
- q = Album.where("great" => true).by_formula(formula)
38
- expect(q.params[:where_clauses]).to be {}
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
- end
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
@@ -1,21 +1,20 @@
1
- require 'airmodel'
2
- require 'fakeweb'
3
- require 'pry'
4
-
5
- def stub_airtable_response!(url, response, method=:get)
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
- FakeWeb.allow_net_connect = false
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.0.0
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-16 00:00:00.000000000 Z
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: fakeweb
70
+ name: vcr
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '1.3'
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: '1.3'
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: '5.0'
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: '5.0'
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.1
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