active_force 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +43 -17
  4. data/lib/active_attr/dirty.rb +3 -0
  5. data/lib/active_force/active_query.rb +32 -2
  6. data/lib/active_force/association.rb +14 -10
  7. data/lib/active_force/association/association.rb +26 -11
  8. data/lib/active_force/association/belongs_to_association.rb +1 -4
  9. data/lib/active_force/association/eager_load_projection_builder.rb +60 -0
  10. data/lib/active_force/association/has_many_association.rb +15 -5
  11. data/lib/active_force/association/relation_model_builder.rb +70 -0
  12. data/lib/active_force/attribute.rb +30 -0
  13. data/lib/active_force/mapping.rb +78 -0
  14. data/lib/active_force/query.rb +2 -8
  15. data/lib/active_force/sobject.rb +79 -95
  16. data/lib/active_force/table.rb +6 -2
  17. data/lib/active_force/version.rb +1 -1
  18. data/lib/generators/active_force/model/model_generator.rb +1 -0
  19. data/lib/generators/active_force/model/templates/model.rb.erb +0 -2
  20. data/spec/active_force/active_query_spec.rb +39 -12
  21. data/spec/active_force/association/relation_model_builder_spec.rb +62 -0
  22. data/spec/active_force/association_spec.rb +53 -88
  23. data/spec/active_force/attribute_spec.rb +27 -0
  24. data/spec/active_force/callbacks_spec.rb +1 -23
  25. data/spec/active_force/mapping_spec.rb +18 -0
  26. data/spec/active_force/query_spec.rb +32 -54
  27. data/spec/active_force/sobject/includes_spec.rb +290 -0
  28. data/spec/active_force/sobject/table_name_spec.rb +0 -21
  29. data/spec/active_force/sobject_spec.rb +212 -29
  30. data/spec/active_force/table_spec.rb +0 -3
  31. data/spec/fixtures/sobject/single_sobject_hash.yml +2 -0
  32. data/spec/spec_helper.rb +10 -4
  33. data/spec/support/restforce_factories.rb +9 -0
  34. data/spec/support/sobjects.rb +97 -0
  35. data/spec/support/whizbang.rb +25 -7
  36. metadata +18 -2
@@ -2,45 +2,24 @@ require 'spec_helper'
2
2
 
3
3
  describe ActiveForce::SObject do
4
4
  describe '#table_name' do
5
-
6
5
  it 'Use the class name adding "__c"' do
7
- class Custom < ActiveForce::SObject
8
- end
9
-
10
6
  expect(Custom.table_name).to eq('Custom__c')
11
7
  end
12
8
 
13
9
  it 'with standard SObject types it does not add the "__c"' do
14
- class Account < ActiveForce::SObject
15
- end
16
-
17
10
  expect(Account.table_name).to eq('Account')
18
11
  end
19
12
 
20
13
  it 'can be enforced in the class definition' do
21
- class EnforcedTableName < ActiveForce::SObject
22
- self.table_name = 'Forced__c'
23
- end
24
-
25
14
  expect(EnforcedTableName.table_name).to eq('Forced__c')
26
15
  end
27
16
 
28
17
  context 'with a namespace' do
29
18
  it "the namespace is not included" do
30
- module Foo
31
- class Bar < ActiveForce::SObject
32
- end
33
- end
34
-
35
19
  expect(Foo::Bar.table_name).to eq('Bar__c')
36
20
  end
37
21
 
38
22
  it 'standard types are inferred correctly' do
39
- module Foo
40
- class Account < ActiveForce::SObject
41
- end
42
- end
43
-
44
23
  expect(Foo::Account.table_name).to eq('Account')
45
24
  end
46
25
  end
@@ -9,17 +9,26 @@ describe ActiveForce::SObject do
9
9
  end
10
10
 
11
11
  describe ".new" do
12
-
13
12
  it 'should assigns values when are passed by parameters' do
14
13
  expect(Whizbang.new({ text: 'some text' }).text).to eq 'some text'
15
14
  end
16
-
17
15
  end
18
16
 
19
17
  describe ".build" do
18
+ let(:sobject){ Whizbang.build sobject_hash }
20
19
 
21
20
  it "build a valid sobject from a JSON" do
22
- expect(Whizbang.build sobject_hash).to be_an_instance_of Whizbang
21
+ expect(sobject).to be_an_instance_of Whizbang
22
+ end
23
+
24
+ it "sets the values' types from the sf_type" do
25
+ expect(sobject.boolean).to be_an_instance_of TrueClass
26
+ expect(sobject.checkbox).to be_an_instance_of FalseClass
27
+ expect(sobject.date).to be_an_instance_of Date
28
+ expect(sobject.datetime).to be_an_instance_of DateTime
29
+ expect(sobject.percent).to be_an_instance_of Float
30
+ expect(sobject.text).to be_an_instance_of String
31
+ expect(sobject.picklist_multiselect).to be_an_instance_of String
23
32
  end
24
33
  end
25
34
 
@@ -61,6 +70,7 @@ describe ActiveForce::SObject do
61
70
  class IceCream < ActiveForce::SObject
62
71
  field :flavors, as: :multi_picklist
63
72
  end
73
+ sundae.changed_attributes.clear
64
74
  sundae.flavors = %w(chocolate vanilla strawberry)
65
75
  end
66
76
 
@@ -84,53 +94,143 @@ describe ActiveForce::SObject do
84
94
  end
85
95
 
86
96
  describe "CRUD" do
87
- subject do
88
- Whizbang.new(id: '1')
89
- end
97
+ let(:instance){ Whizbang.new(id: '1') }
90
98
 
91
99
  describe '#update' do
92
- before do
93
- expected_args = [
94
- Whizbang.table_name,
95
- {'Text_Label' => 'some text', 'Boolean_Label' => false, 'Id' => '1'}
96
- ]
97
- expect(client).to receive(:update!).with(*expected_args).and_return('id')
100
+
101
+ context 'with valid attributes' do
102
+ before do
103
+ expected_args = [
104
+ Whizbang.table_name,
105
+ {'Text_Label' => 'some text', 'Boolean_Label' => false, 'Id' => '1', "Updated_From__c"=>"Rails"}
106
+ ]
107
+ expect(client).to receive(:update!).with(*expected_args).and_return('id')
108
+ end
109
+
110
+ it 'delegates to the Client with create! and sets the id' do
111
+ expect(instance.update( text: 'some text', boolean: false )).to eq(true)
112
+ expect(instance.text).to eq('some text')
113
+ end
114
+ end
115
+
116
+ context 'with invalid attributes' do
117
+ it 'sets the error on the instance' do
118
+ expect(instance.update( boolean: true )).to eq(false)
119
+ expect(instance.errors).to be_present
120
+ expect(instance.errors.full_messages.count).to eq(1)
121
+ expect(instance.errors.full_messages[0]).to eq("Percent can't be blank")
122
+ end
123
+ end
124
+ end
125
+
126
+ describe ".update!" do
127
+ context 'with valid attributes' do
128
+ describe 'and without a ClientError' do
129
+ before do
130
+ expected_args = [
131
+ Whizbang.table_name,
132
+ {'Text_Label' => 'some text', 'Boolean_Label' => false, 'Id' => '1', "Updated_From__c"=>"Rails"}
133
+ ]
134
+ expect(client).to receive(:update!).with(*expected_args).and_return('id')
135
+ end
136
+ it 'saves successfully' do
137
+ expect(instance.update!( text: 'some text', boolean: false )).to eq(true)
138
+ expect(instance.text).to eq('some text')
139
+ end
140
+ end
141
+
142
+ describe 'and with a ClientError' do
143
+ let(:faraday_error){ Faraday::Error::ClientError.new('Some String') }
144
+
145
+ before{ expect(client).to receive(:update!).and_raise(faraday_error) }
146
+
147
+ it 'raises an error' do
148
+ expect{ instance.update!( text: 'some text', boolean: false ) }.to raise_error(Faraday::Error::ClientError)
149
+ end
150
+ end
98
151
  end
99
152
 
100
- it 'delegates to the Client with create!' do
101
- expect(subject.update({ text: 'some text', boolean: false })).to be_a Whizbang
153
+ context 'with invalid attributes' do
154
+ let(:instance){ Whizbang.new boolean: true }
155
+
156
+ it 'raises an error' do
157
+ expect{ instance.update!( text: 'some text', boolean: true ) }.to raise_error(ActiveForce::RecordInvalid)
158
+ end
102
159
  end
103
160
  end
104
161
 
105
162
  describe '#create' do
106
- before do
107
- expect(client).to receive(:create!).and_return('id')
163
+ context 'with valid attributes' do
164
+ before do
165
+ expect(client).to receive(:create!).and_return('id')
166
+ end
167
+
168
+ it 'delegates to the Client with create! and sets the id' do
169
+ expect(instance.create).to be_instance_of(Whizbang)
170
+ expect(instance.id).to eq('id')
171
+ end
172
+ end
173
+
174
+
175
+ context 'with invalid attributes' do
176
+ let(:instance){ Whizbang.new boolean: true }
177
+
178
+ it 'sets the error on the instance' do
179
+ expect(instance.create).to be_instance_of(Whizbang)
180
+ expect(instance.id).to eq(nil)
181
+ expect(instance.errors).to be_present
182
+ expect(instance.errors.full_messages.count).to eq(1)
183
+ expect(instance.errors.full_messages[0]).to eq("Percent can't be blank")
184
+ end
108
185
  end
186
+ end
187
+
188
+ describe '#create!' do
189
+ context 'with valid attributes' do
190
+ describe 'and without a ClientError' do
191
+
192
+ before{ expect(client).to receive(:create!).and_return('id') }
109
193
 
110
- it 'delegates to the Client with create!' do
111
- subject.create
194
+ it 'saves successfully' do
195
+ expect(instance.create!).to be_instance_of(Whizbang)
196
+ expect(instance.id).to eq('id')
197
+ end
198
+ end
199
+
200
+ describe 'and with a ClientError' do
201
+ let(:faraday_error){ Faraday::Error::ClientError.new('Some String') }
202
+
203
+ before{ expect(client).to receive(:create!).and_raise(faraday_error) }
204
+
205
+ it 'raises an error' do
206
+ expect{ instance.create! }.to raise_error(Faraday::Error::ClientError)
207
+ end
208
+ end
112
209
  end
113
210
 
114
- it 'sets the id' do
115
- subject.create
116
- expect(subject.id).to eq('id')
211
+ context 'with invalid attributes' do
212
+ let(:instance){ Whizbang.new boolean: true }
213
+
214
+ it 'raises an error' do
215
+ expect{ instance.create! }.to raise_error(ActiveForce::RecordInvalid)
216
+ end
117
217
  end
118
218
  end
119
219
 
120
220
  describe "#destroy" do
121
221
  it "should send client :destroy! with its id" do
122
222
  expect(client).to receive(:destroy!).with 'Whizbang__c', '1'
123
- subject.destroy
223
+ instance.destroy
124
224
  end
125
225
  end
126
226
 
127
227
  describe 'self.create' do
128
228
  before do
129
- expect(client).to receive(:create!).with(Whizbang.table_name, 'Text_Label' => 'some text').and_return('id')
229
+ expect(client).to receive(:create!).with(Whizbang.table_name, 'Text_Label' => 'some text', 'Updated_From__c'=>'Rails').and_return('id')
130
230
  end
131
231
 
132
232
  it 'should create a new instance' do
133
- expect(Whizbang.create({ text: 'some text' })).to be_a Whizbang
233
+ expect(Whizbang.create(text: 'some text')).to be_instance_of(Whizbang)
134
234
  end
135
235
  end
136
236
  end
@@ -151,7 +251,7 @@ describe ActiveForce::SObject do
151
251
 
152
252
  describe "#find_by" do
153
253
  it "should query the client, with the SFDC field names and correctly enclosed values" do
154
- expect(client).to receive(:query).with("SELECT #{Whizbang.fields.join ', '} FROM Whizbang__c WHERE Id = 123 AND Text_Label = 'foo' LIMIT 1")
254
+ expect(client).to receive(:query).with("SELECT #{Whizbang.fields.join ', '} FROM Whizbang__c WHERE (Id = 123) AND (Text_Label = 'foo') LIMIT 1")
155
255
  Whizbang.find_by id: 123, text: "foo"
156
256
  end
157
257
  end
@@ -164,13 +264,11 @@ describe ActiveForce::SObject do
164
264
  let(:territory){ Territory.new(id: '1', quota_id: '1') }
165
265
 
166
266
  before do
167
- Territory.belongs_to :quota, model: Quota
168
- Territory.field :quota_id, from: 'Quota_Id'
169
267
  allow(ActiveForce::SObject).to receive(:sfdc_client).and_return client
170
268
  end
171
269
 
172
270
  it 'clears cached associations' do
173
- soql = "SELECT Id, Bar_Id__c FROM Quota__c WHERE Id = '1' LIMIT 1"
271
+ soql = "SELECT Id, Bar_Id__c FROM Quota__c WHERE (Id = '1') LIMIT 1"
174
272
  expect(client).to receive(:query).twice.with soql
175
273
  allow(Territory).to receive(:find){ territory }
176
274
  territory.quota
@@ -180,7 +278,6 @@ describe ActiveForce::SObject do
180
278
  end
181
279
 
182
280
  it "refreshes the object's attributes" do
183
- Territory.field :name, from: 'Name'
184
281
  territory.name = 'Walter'
185
282
  expect(territory.name).to eq 'Walter'
186
283
  territory.reload
@@ -212,4 +309,90 @@ describe ActiveForce::SObject do
212
309
  end
213
310
  end
214
311
  end
312
+
313
+ describe 'logger output' do
314
+ let(:instance){ Whizbang.new }
315
+
316
+ before do
317
+ allow(instance).to receive(:create!).and_raise(Faraday::Error::ClientError.new(double))
318
+ end
319
+
320
+ it 'catches and logs the error' do
321
+ expect(instance).to receive(:logger_output).and_return(false)
322
+ instance.save
323
+ end
324
+ end
325
+
326
+ describe ".save!" do
327
+ let(:instance){ Whizbang.new }
328
+
329
+ context 'with valid attributes' do
330
+ describe 'and without a ClientError' do
331
+ before{ expect(client).to receive(:create!).and_return('id') }
332
+ it 'saves successfully' do
333
+ expect(instance.save!).to eq(true)
334
+ end
335
+ end
336
+
337
+ describe 'and with a ClientError' do
338
+ let(:faraday_error){ Faraday::Error::ClientError.new('Some String') }
339
+
340
+ before{ expect(client).to receive(:create!).and_raise(faraday_error) }
341
+
342
+ it 'raises an error' do
343
+ expect{ instance.save! }.to raise_error(Faraday::Error::ClientError)
344
+ end
345
+ end
346
+ end
347
+
348
+ context 'with invalid attributes' do
349
+ let(:instance){ Whizbang.new boolean: true }
350
+
351
+ it 'raises an error' do
352
+ expect{ instance.save! }.to raise_error(ActiveForce::RecordInvalid)
353
+ end
354
+ end
355
+ end
356
+
357
+ describe ".save" do
358
+ let(:instance){ Whizbang.new }
359
+
360
+ context 'with valid attributes' do
361
+ describe 'and without a ClientError' do
362
+ before{ expect(client).to receive(:create!).and_return('id') }
363
+ it 'saves successfully' do
364
+ expect(instance.save).to eq(true)
365
+ end
366
+ end
367
+
368
+ describe 'and with a ClientError' do
369
+ let(:faraday_error){ Faraday::Error::ClientError.new('Some String') }
370
+ before{ expect(client).to receive(:create!).and_raise(faraday_error) }
371
+ it 'returns false' do
372
+ expect(instance.save).to eq(false)
373
+ end
374
+ it 'sets the error on the instance' do
375
+ instance.save
376
+ expect(instance.errors).to be_present
377
+ expect(instance.errors.full_messages.count).to eq(1)
378
+ expect(instance.errors.full_messages[0]).to eq('Some String')
379
+ end
380
+ end
381
+ end
382
+
383
+ context 'with invalid attributes' do
384
+ let(:instance){ Whizbang.new boolean: true }
385
+
386
+ it 'does not save' do
387
+ expect(instance.save).to eq(false)
388
+ end
389
+
390
+ it 'sets the error on the instance' do
391
+ instance.save
392
+ expect(instance.errors).to be_present
393
+ expect(instance.errors.full_messages.count).to eq(1)
394
+ expect(instance.errors.full_messages[0]).to eq("Percent can't be blank")
395
+ end
396
+ end
397
+ end
215
398
  end
@@ -1,9 +1,7 @@
1
1
  require 'spec_helper'
2
- require 'active_force/table'
3
2
 
4
3
  describe ActiveForce::Table do
5
4
  describe '#table_name' do
6
-
7
5
  let(:table) { ActiveForce::Table }
8
6
 
9
7
  it 'Use the class name adding "__c"' do
@@ -23,6 +21,5 @@ describe ActiveForce::Table do
23
21
  expect(table.new('Foo::Account').name).to eq('Account')
24
22
  end
25
23
  end
26
-
27
24
  end
28
25
  end
@@ -6,6 +6,8 @@ Text_Label: 'Hi there!'
6
6
  Date_Label: '2010-01-01'
7
7
  DateTime_Label: '2011-07-07T00:37:00.000+0000'
8
8
  Picklist_Multiselect_Label: 'four;six'
9
+ Percent_Label: 20
10
+ Boolean_Label: true
9
11
 
10
12
  ParentWhizbang__r:
11
13
  Name: 'Parent Whizbang'
@@ -2,12 +2,18 @@ require "codeclimate-test-reporter"
2
2
  CodeClimate::TestReporter.start
3
3
 
4
4
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
5
- require 'restforce'
6
5
  require 'active_force'
7
6
  Dir["./spec/support/**/*"].sort.each { |f| require f }
8
7
  require 'pry'
9
8
 
10
- class Territory < ActiveForce::SObject; end
11
- class Quota < ActiveForce::SObject
12
- field :id, from: 'Bar_Id__c'
9
+ ActiveSupport::Inflector.inflections do |inflect|
10
+ inflect.plural 'quota', 'quotas'
11
+ inflect.plural 'Quota', 'Quotas'
12
+ inflect.singular 'quota', 'quota'
13
+ inflect.singular 'Quota', 'Quota'
14
+ end
15
+
16
+ RSpec.configure do |config|
17
+ config.order = :random
18
+ config.include RestforceFactories
13
19
  end
@@ -0,0 +1,9 @@
1
+ module RestforceFactories
2
+ def build_restforce_collection(array)
3
+ Restforce::Collection.new({ 'records' => array }, nil)
4
+ end
5
+
6
+ def build_restforce_sobject(hash)
7
+ Restforce::SObject.new(hash)
8
+ end
9
+ end
@@ -0,0 +1,97 @@
1
+
2
+ class Comment < ActiveForce::SObject
3
+ self.table_name = "Comment__c"
4
+ field :post_id, from: "PostId"
5
+ field :poster_id, from: 'PosterId__c'
6
+ field :fancy_post_id, from: 'FancyPostId'
7
+ field :body
8
+ belongs_to :post
9
+ end
10
+ class Post < ActiveForce::SObject
11
+ self.table_name = "Post__c"
12
+ field :title
13
+ has_many :comments
14
+ has_many :impossible_comments, model: Comment, scoped_as: ->{ where('1 = 0') }
15
+ has_many :reply_comments, model: Comment, scoped_as: ->(post){ where(body: "RE: #{post.title}").order('CreationDate DESC') }
16
+ has_many :ugly_comments, { model: Comment }
17
+ has_many :poster_comments, { foreign_key: :poster_id, model: Comment }
18
+ end
19
+ class Territory < ActiveForce::SObject
20
+ field :quota_id, from: "Quota__c"
21
+ field :name, from: 'Name'
22
+ belongs_to :quota
23
+ end
24
+ class PrezClub < ActiveForce::SObject
25
+ field :quota_id, from: 'QuotaId'
26
+ belongs_to :quota
27
+ end
28
+ class Quota < ActiveForce::SObject
29
+ field :id, from: 'Bar_Id__c'
30
+ has_many :prez_clubs
31
+ has_many :territories
32
+ end
33
+ class Opportunity < ActiveForce::SObject
34
+ field :account_id, from: 'AccountId'
35
+ belongs_to :account
36
+ end
37
+ class Account < ActiveForce::SObject
38
+ field :owner_id, from: 'OwnerId'
39
+ has_many :opportunities
40
+ belongs_to :owner
41
+ end
42
+ class Owner < ActiveForce::SObject
43
+ has_many :accounts
44
+ end
45
+ class Custom < ActiveForce::SObject; end
46
+ class EnforcedTableName < ActiveForce::SObject
47
+ self.table_name = 'Forced__c'
48
+ end
49
+
50
+ module Foo
51
+ class Bar < ActiveForce::SObject; end
52
+ class Opportunity < ActiveForce::SObject
53
+ field :account_id, from: 'AccountId'
54
+ field :partner_account_id, from: 'Partner_Account_Id__c'
55
+ end
56
+ class Account < ActiveForce::SObject
57
+ has_many :opportunities, model: Foo::Opportunity
58
+ has_many :partner_opportunities, foreign_key: :partner_account_id, model: Foo::Opportunity
59
+ end
60
+ class Lead < ActiveForce::SObject; end
61
+ class Attachment < ActiveForce::SObject
62
+ field :lead_id, from: 'Lead_Id__c'
63
+ field :fancy_lead_id, from: 'LeadId'
64
+ belongs_to :lead, model: Foo::Lead
65
+ belongs_to :fancy_lead, model: Foo::Lead, foreign_key: :fancy_lead_id
66
+ end
67
+ end
68
+
69
+ module Salesforce
70
+ class PrezClub < ActiveForce::SObject
71
+ field :quota_id, from: 'QuotaId'
72
+ end
73
+ class Quota < ActiveForce::SObject
74
+ has_many :prez_clubs, model: PrezClub
75
+ end
76
+ class Widget < ActiveForce::SObject
77
+ self.table_name = 'Tegdiw__c'
78
+ end
79
+ class Territory < ActiveForce::SObject
80
+ field :quota_id, from: "QuotaId"
81
+ field :widget_id, from: 'WidgetId'
82
+ belongs_to :quota, model: Salesforce::Quota, foreign_key: :quota_id
83
+ belongs_to :widget, model: Salesforce::Widget, foreign_key: :widget_id
84
+ end
85
+ class User < ActiveForce::SObject
86
+ end
87
+ class Opportunity < ActiveForce::SObject
88
+ field :owner_id, from: 'OwnerId'
89
+ field :account_id, from: 'AccountId'
90
+ field :business_partner
91
+ belongs_to :owner, model: Salesforce::User, foreign_key: :owner_id, relationship_name: 'Owner'
92
+ end
93
+ class Account < ActiveForce::SObject
94
+ field :business_partner
95
+ has_many :partner_opportunities, model: Opportunity, scoped_as: ->(account){ where(business_partner: account.business_partner).includes(:owner) }
96
+ end
97
+ end