active_force 0.6.1 → 0.7.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.
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