active_force 0.7.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +107 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.mailmap +3 -0
  6. data/CHANGELOG.md +115 -42
  7. data/CODEOWNERS +2 -0
  8. data/Gemfile +0 -1
  9. data/README.md +100 -21
  10. data/active_force.gemspec +11 -4
  11. data/lib/active_force/active_query.rb +107 -6
  12. data/lib/active_force/association/association.rb +47 -3
  13. data/lib/active_force/association/belongs_to_association.rb +25 -11
  14. data/lib/active_force/association/eager_load_projection_builder.rb +9 -3
  15. data/lib/active_force/association/has_many_association.rb +19 -19
  16. data/lib/active_force/association/has_one_association.rb +30 -0
  17. data/lib/active_force/association/relation_model_builder.rb +1 -1
  18. data/lib/active_force/association.rb +6 -4
  19. data/lib/active_force/{attribute.rb → field.rb} +3 -3
  20. data/lib/active_force/mapping.rb +6 -32
  21. data/lib/active_force/query.rb +21 -2
  22. data/lib/active_force/sobject.rb +74 -28
  23. data/lib/active_force/version.rb +3 -1
  24. data/lib/active_force.rb +2 -0
  25. data/lib/active_model/type/salesforce/multipicklist.rb +29 -0
  26. data/lib/active_model/type/salesforce/percent.rb +22 -0
  27. data/lib/generators/active_force/model/model_generator.rb +32 -21
  28. data/lib/generators/active_force/model/templates/model.rb.erb +3 -1
  29. data/spec/active_force/active_query_spec.rb +200 -8
  30. data/spec/active_force/association/relation_model_builder_spec.rb +22 -0
  31. data/spec/active_force/association_spec.rb +252 -9
  32. data/spec/active_force/field_spec.rb +34 -0
  33. data/spec/active_force/query_spec.rb +26 -0
  34. data/spec/active_force/sobject/includes_spec.rb +10 -10
  35. data/spec/active_force/sobject_spec.rb +221 -14
  36. data/spec/fixtures/sobject/single_sobject_hash.yml +1 -1
  37. data/spec/spec_helper.rb +5 -2
  38. data/spec/support/bangwhiz.rb +7 -0
  39. data/spec/support/restforce_factories.rb +1 -1
  40. data/spec/support/sobjects.rb +17 -1
  41. data/spec/support/whizbang.rb +2 -2
  42. metadata +64 -26
  43. data/lib/active_attr/dirty.rb +0 -24
  44. data/spec/active_force/attribute_spec.rb +0 -27
@@ -9,6 +9,14 @@ describe ActiveForce::SObject do
9
9
  Comment.new(id: "1", post_id: "1")
10
10
  end
11
11
 
12
+ let :has_one_parent do
13
+ HasOneParent.new(id: '1', comment: "BAR")
14
+ end
15
+
16
+ let :has_one_child do
17
+ HasOneChild.new(id: '1', has_one_parent_id: '1')
18
+ end
19
+
12
20
  let :client do
13
21
  double("sfdc_client", query: [Restforce::Mash.new("Id" => 1)])
14
22
  end
@@ -39,6 +47,19 @@ describe ActiveForce::SObject do
39
47
  end
40
48
  end
41
49
 
50
+ context 'when primary key is blank' do
51
+ let(:post) { Post.new }
52
+
53
+ it 'does not make any queries' do
54
+ post.comments.to_a
55
+ expect(client).not_to have_received :query
56
+ end
57
+
58
+ it 'returns empty' do
59
+ expect(post.comments.to_a).to be_empty
60
+ end
61
+ end
62
+
42
63
  context 'when the SObject is namespaced' do
43
64
  let(:account){ Foo::Account.new(id: '1') }
44
65
 
@@ -85,6 +106,197 @@ describe ActiveForce::SObject do
85
106
  soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (PostId = '1')"
86
107
  expect(post.comments.to_s).to eq soql
87
108
  end
109
+
110
+ context 'when passing `model` option' do
111
+ before do
112
+ allow(Comment).to receive(:where).once.and_return([comment])
113
+ end
114
+
115
+ it 'allows passing as a constant' do
116
+ Post.has_many :comments, model: Comment
117
+ expect { post.comments }.to_not raise_error
118
+ end
119
+
120
+ it 'allows passing as a string' do
121
+ Post.has_many :comments, model: 'Comment'
122
+ expect { post.comments }.to_not raise_error
123
+ end
124
+ end
125
+ end
126
+
127
+ describe "has_one_query" do
128
+ it "should respond to relation method" do
129
+ expect(has_one_parent).to respond_to(:has_one_child)
130
+ end
131
+
132
+ it "should return a the correct child object" do
133
+ expect(has_one_parent.has_one_child).to be_a HasOneChild
134
+ end
135
+
136
+ it 'makes only one API call to fetch the associated object' do
137
+ has_one_parent.has_one_child.id
138
+ has_one_parent.has_one_child.id
139
+ expect(client).to have_received(:query).once
140
+ end
141
+
142
+ it 'queries for a single record with the correct foreign key' do
143
+ expected = <<~SOQL.squish
144
+ SELECT Id, has_one_parent_id__c, FancyParentId FROM HasOneChild__c WHERE (has_one_parent_id__c = '1') LIMIT 1
145
+ SOQL
146
+ has_one_parent.has_one_child
147
+ expect(client).to have_received(:query).with(expected)
148
+ end
149
+
150
+ context 'when primary key is blank' do
151
+ let(:parent) { HasOneParent.new }
152
+
153
+ it 'does not make any queries' do
154
+ parent.has_one_child
155
+ expect(client).not_to have_received :query
156
+ end
157
+
158
+ it 'returns nil' do
159
+ expect(parent.has_one_child).to be_nil
160
+ end
161
+ end
162
+
163
+ describe "assignments" do
164
+ let(:has_one) do
165
+ has_one_parent = HasOneParent.new(id: '1')
166
+ has_one_parent.has_one_child = HasOneChild.new(id: '1')
167
+ has_one_parent
168
+ end
169
+
170
+ before do
171
+ expect(client).to_not receive(:query)
172
+ end
173
+
174
+ it 'accepts assignment of an existing object as an association' do
175
+ expect(client).to_not receive(:query)
176
+ other_child = HasOneChild.new(id: '2')
177
+ has_one.has_one_child = other_child
178
+ expect(has_one.has_one_child.has_one_parent_id).to eq has_one.id
179
+ expect(has_one.has_one_child).to eq other_child
180
+ end
181
+
182
+ it 'uses first element if given Array' do
183
+ first_child = HasOneChild.new(id: '2')
184
+ has_one.has_one_child = [first_child, HasOneChild.new(id: '3')]
185
+ expect(has_one.has_one_child.has_one_parent_id).to eq has_one.id
186
+ expect(has_one.has_one_child).to eq first_child
187
+ end
188
+
189
+ it 'can desassociate an object by setting it as nil' do
190
+ old_child = has_one.has_one_child
191
+ has_one.has_one_child = nil
192
+ expect(old_child.has_one_parent_id).to eq nil
193
+ expect(has_one.has_one_child).to eq nil
194
+ end
195
+
196
+ context 'when primary key is blank' do
197
+ let(:child) { HasOneChild.new }
198
+ let(:parent) { HasOneParent.new }
199
+
200
+ it 'accepts assignment' do
201
+ parent.has_one_child = child
202
+ expect(parent.has_one_child).to eq(child)
203
+ end
204
+
205
+ it 'accepts reassignment' do
206
+ parent.has_one_child = child
207
+ other_child = HasOneChild.new(id: 'x')
208
+ parent.has_one_child = other_child
209
+ expect(parent.has_one_child).to eq(other_child)
210
+ end
211
+
212
+ it 'accepts nil assignment' do
213
+ parent.has_one_child = child
214
+ parent.has_one_child = nil
215
+ expect(parent.has_one_child).to be_nil
216
+ end
217
+
218
+ it 'assigns the first element if given an array' do
219
+ parent.has_one_child = [child, 'something else']
220
+ expect(parent.has_one_child).to eq(child)
221
+ end
222
+ end
223
+ end
224
+
225
+ context 'when the SObject is namespaced' do
226
+ let(:attachment){ Foo::Attachment.new(id: '1', lead_id: '2') }
227
+ let(:lead){ Foo::Lead.new(id: '2') }
228
+
229
+ it 'generates the correct query' do
230
+ expect(client).to receive(:query).with("SELECT Id, Lead_Id__c, LeadId FROM Attachment WHERE (Lead_Id__c = '2') LIMIT 1")
231
+ lead.attachment
232
+ end
233
+
234
+ it 'instantiates the correct object' do
235
+ expect(lead.attachment).to be_instance_of(Foo::Attachment)
236
+ end
237
+
238
+ context 'when given a foreign key' do
239
+ let(:lead) { Foo::Lead.new(id: '2') }
240
+
241
+ it 'generates the correct query' do
242
+ expect(client).to receive(:query).with("SELECT Id, Lead_Id__c, LeadId FROM Attachment WHERE (LeadId = '2') LIMIT 1")
243
+ lead.fancy_attachment
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ describe 'has_one(options)' do
250
+ it 'allows passing a foreign key' do
251
+ HasOneParent.has_one :has_one_child, foreign_key: :fancy_parent_id
252
+ allow(has_one_parent).to receive(:id).and_return "2"
253
+ expect(client).to receive(:query).with("SELECT Id, has_one_parent_id__c, FancyParentId FROM HasOneChild__c WHERE (FancyParentId = '2') LIMIT 1")
254
+ has_one_parent.has_one_child
255
+ HasOneParent.has_one :has_one_child, foreign_key: :has_one_parent_id # reset association to original value
256
+ end
257
+
258
+ context 'when passing `model` option' do
259
+ before do
260
+ allow(HasOneChild).to receive(:find_by).and_return(has_one_child)
261
+ end
262
+
263
+ it 'allows passing as a constant' do
264
+ HasOneParent.has_one :has_one_child, model: HasOneChild
265
+ expect { has_one_parent.has_one_child }.to_not raise_error
266
+ end
267
+
268
+ it 'allows passing as a string' do
269
+ HasOneParent.has_one :has_one_child, model: 'HasOneChild'
270
+ expect { has_one_parent.has_one_child }.to_not raise_error
271
+ end
272
+ end
273
+
274
+ context "when passing 'scoped_as' option" do
275
+ it 'makes a only single query if called more than once' do
276
+ post.last_comment
277
+ post.last_comment
278
+ expect(client).to have_received(:query).once
279
+ end
280
+
281
+ it 'applies the scope to the query' do
282
+ expected = <<~SOQL.squish
283
+ SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c
284
+ WHERE (NOT ((Body__c = NULL))) AND (PostId = '1') ORDER BY CreatedDate DESC LIMIT 1
285
+ SOQL
286
+ post.last_comment
287
+ expect(client).to have_received(:query).with(expected)
288
+ end
289
+
290
+ it 'applies the scope to the query if the lambda takes an argument' do
291
+ post.title = 'test_post_title'
292
+ expected = <<~SOQL.squish
293
+ SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c
294
+ WHERE (Body__c = 'test_post_title') AND (PostId = '1') LIMIT 1
295
+ SOQL
296
+ post.repeat_comment
297
+ expect(client).to have_received(:query).with(expected)
298
+ end
299
+ end
88
300
  end
89
301
 
90
302
  describe "belongs_to" do
@@ -92,20 +304,25 @@ describe ActiveForce::SObject do
92
304
  expect(comment.post).to be_instance_of(Post)
93
305
  end
94
306
 
95
- it "should allow to pass a foreign key as options" do
96
- Comment.belongs_to :post, foreign_key: :fancy_post_id
97
- allow(comment).to receive(:fancy_post_id).and_return "2"
98
- expect(client).to receive(:query).with("SELECT Id, Title__c FROM Post__c WHERE (Id = '2') LIMIT 1")
99
- comment.post
100
- Comment.belongs_to :post # reset association to original value
101
- end
102
-
103
307
  it 'makes only one API call to fetch the associated object' do
104
308
  expect(client).to receive(:query).once
105
309
  comment.post
106
310
  comment.post
107
311
  end
108
312
 
313
+ context 'when foreign key is blank' do
314
+ let(:comment) { Comment.new(id: '1') }
315
+
316
+ it 'does not make any queries' do
317
+ comment.post
318
+ expect(client).not_to have_received :query
319
+ end
320
+
321
+ it 'returns nil' do
322
+ expect(comment.post).to be_nil
323
+ end
324
+ end
325
+
109
326
  describe "assignments" do
110
327
  let(:comment) do
111
328
  comment = Comment.new(id: '1')
@@ -116,7 +333,7 @@ describe ActiveForce::SObject do
116
333
  before do
117
334
  expect(client).to_not receive(:query)
118
335
  end
119
-
336
+
120
337
  it 'accepts assignment of an existing object as an association' do
121
338
  expect(client).to_not receive(:query)
122
339
  other_post = Post.new(id: "2")
@@ -154,4 +371,30 @@ describe ActiveForce::SObject do
154
371
  end
155
372
  end
156
373
  end
374
+
375
+ describe 'belongs_to(options)' do
376
+ it 'allows passing a foreign key' do
377
+ Comment.belongs_to :post, foreign_key: :fancy_post_id
378
+ allow(comment).to receive(:fancy_post_id).and_return "2"
379
+ expect(client).to receive(:query).with("SELECT Id, Title__c FROM Post__c WHERE (Id = '2') LIMIT 1")
380
+ comment.post
381
+ Comment.belongs_to :post # reset association to original value
382
+ end
383
+
384
+ context 'when passing `model` option' do
385
+ before do
386
+ allow(Post).to receive(:find).once.and_return(post)
387
+ end
388
+
389
+ it 'allows passing as a constant' do
390
+ Comment.belongs_to :post, model: Post
391
+ expect { comment.post.id }.to_not raise_error
392
+ end
393
+
394
+ it 'allows passing as a string' do
395
+ Comment.belongs_to :post, model: 'Post'
396
+ expect { comment.post.id }.to_not raise_error
397
+ end
398
+ end
399
+ end
157
400
  end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveForce::Field do
4
+ let(:field) { ActiveForce::Field }
5
+
6
+ describe 'initialize' do
7
+ let(:some_field) { field.new(:some_field) }
8
+
9
+ it 'should set "from" and "as" as default' do
10
+ expect(some_field.sfdc_name).to eq 'Some_Field__c'
11
+ expect(some_field.as).to eq :string
12
+ end
13
+
14
+ it 'should take values from the option parameter' do
15
+ other_field = field.new(:other_field, sfdc_name: 'OT__c', as: :integer)
16
+ expect(other_field.sfdc_name).to eq 'OT__c'
17
+ expect(other_field.as).to eq :integer
18
+ end
19
+ end
20
+
21
+ describe 'when the field is' do
22
+ it 'a datetime should return a string like "YYYY-MM-DDTHH:MM:SSZ"' do
23
+ current_time = DateTime.now
24
+ names = field.new(:time, as: :datetime)
25
+ expect(names.value_for_hash current_time).to eq current_time.to_fs(:iso8601)
26
+ end
27
+
28
+ it 'a datetime field whose value is nil' do
29
+ current_time = nil
30
+ names = field.new(:time, as: :datetime)
31
+ expect(names.value_for_hash current_time).to be_nil
32
+ end
33
+ end
34
+ end
@@ -48,6 +48,22 @@ describe ActiveForce::Query do
48
48
  end
49
49
  end
50
50
 
51
+ describe ".not" do
52
+ let(:subquery) { ActiveForce::Query.new 'table_name' }
53
+
54
+ it 'should add a not condition' do
55
+ expect(query.not(['condition1 = 1']).to_s).to eq "SELECT Id, name, etc FROM table_name WHERE (NOT ((condition1 = 1)))"
56
+ end
57
+ end
58
+
59
+ describe ".or" do
60
+ let(:subquery) { ActiveForce::Query.new 'table_name' }
61
+
62
+ it 'should create an or condition' do
63
+ expect(query.where('condition1 = 1').where('condition2 = 2').or(subquery.where('condition3 = 3')).to_s).to eq "SELECT Id, name, etc FROM table_name WHERE (((condition1 = 1) AND (condition2 = 2)) OR ((condition3 = 3)))"
64
+ end
65
+ end
66
+
51
67
  describe ".limit" do
52
68
  it "should add a limit to a query" do
53
69
  expect(query.limit("25").to_s).to eq "SELECT Id, name, etc FROM table_name LIMIT 25"
@@ -123,4 +139,14 @@ describe ActiveForce::Query do
123
139
  expect(query.where("name = 'cool'").count.to_s).to eq "SELECT count(Id) FROM table_name WHERE (name = 'cool')"
124
140
  end
125
141
  end
142
+
143
+ describe ".sum" do
144
+ it "should return the query for summing the desired column" do
145
+ expect(query.sum(:field1).to_s).to eq 'SELECT sum(field1) FROM table_name'
146
+ end
147
+
148
+ it "should work with a condition" do
149
+ expect(query.where("name = 'cool'").sum(:field1).to_s).to eq "SELECT sum(field1) FROM table_name WHERE (name = 'cool')"
150
+ end
151
+ end
126
152
  end
@@ -46,7 +46,7 @@ module ActiveForce
46
46
  context 'with namespaced SObjects' do
47
47
  it 'queries the API for the associated record' do
48
48
  soql = Salesforce::Territory.includes(:quota).where(id: '123').to_s
49
- expect(soql).to eq "SELECT Id, QuotaId, WidgetId, Quota__r.Id FROM Territory WHERE (Id = '123')"
49
+ expect(soql).to eq "SELECT Id, QuotaId, WidgetId, QuotaId.Id FROM Territory WHERE (Id = '123')"
50
50
  end
51
51
 
52
52
  it "queries the API once to retrieve the object and its related one" do
@@ -54,7 +54,7 @@ module ActiveForce
54
54
  "Id" => "123",
55
55
  "QuotaId" => "321",
56
56
  "WidgetId" => "321",
57
- "Quota__r" => {
57
+ "QuotaId" => {
58
58
  "Id" => "321"
59
59
  }
60
60
  })]
@@ -88,7 +88,7 @@ module ActiveForce
88
88
 
89
89
  context 'when the class name does not match the SFDC entity name' do
90
90
  let(:expected_soql) do
91
- "SELECT Id, QuotaId, WidgetId, Tegdiw__r.Id FROM Territory WHERE (Id = '123')"
91
+ "SELECT Id, QuotaId, WidgetId, WidgetId.Id FROM Territory WHERE (Id = '123')"
92
92
  end
93
93
 
94
94
  it 'queries the API for the associated record' do
@@ -100,7 +100,7 @@ module ActiveForce
100
100
  response = [build_restforce_sobject({
101
101
  "Id" => "123",
102
102
  "WidgetId" => "321",
103
- "Tegdiw__r" => {
103
+ "WidgetId" => {
104
104
  "Id" => "321"
105
105
  }
106
106
  })]
@@ -115,7 +115,7 @@ module ActiveForce
115
115
  context 'child to several parents' do
116
116
  it 'queries the API for associated records' do
117
117
  soql = Salesforce::Territory.includes(:quota, :widget).where(id: '123').to_s
118
- expect(soql).to eq "SELECT Id, QuotaId, WidgetId, Quota__r.Id, Tegdiw__r.Id FROM Territory WHERE (Id = '123')"
118
+ expect(soql).to eq "SELECT Id, QuotaId, WidgetId, QuotaId.Id, WidgetId.Id FROM Territory WHERE (Id = '123')"
119
119
  end
120
120
 
121
121
  it "queries the API once to retrieve the object and its assocations" do
@@ -123,10 +123,10 @@ module ActiveForce
123
123
  "Id" => "123",
124
124
  "QuotaId" => "321",
125
125
  "WidgetId" => "321",
126
- "Quota__r" => {
126
+ "QuotaId" => {
127
127
  "Id" => "321"
128
128
  },
129
- "Tegdiw__r" => {
129
+ "WidgetId" => {
130
130
  "Id" => "321"
131
131
  }
132
132
  })]
@@ -263,7 +263,7 @@ module ActiveForce
263
263
  describe 'mixing belongs_to and has_many' do
264
264
  it 'formulates the correct SOQL query' do
265
265
  soql = Account.includes(:opportunities, :owner).where(id: '123').to_s
266
- expect(soql).to eq "SELECT Id, OwnerId, (SELECT Id, AccountId FROM Opportunities), Owner__r.Id FROM Account WHERE (Id = '123')"
266
+ expect(soql).to eq "SELECT Id, OwnerId, (SELECT Id, AccountId FROM Opportunities), OwnerId.Id FROM Account WHERE (Id = '123')"
267
267
  end
268
268
 
269
269
  it 'builds the associated objects and caches them' do
@@ -273,12 +273,12 @@ module ActiveForce
273
273
  {'Id' => '213', 'AccountId' => '123'},
274
274
  {'Id' => '214', 'AccountId' => '123'}
275
275
  ]),
276
- 'Owner__r' => {
276
+ 'OwnerId' => {
277
277
  'Id' => '321'
278
278
  }
279
279
  })]
280
280
  allow(client).to receive(:query).once.and_return response
281
- account = Account.includes(:opportunities).find '123'
281
+ account = Account.includes(:opportunities, :owner).find '123'
282
282
  expect(account.opportunities).to be_an Array
283
283
  expect(account.opportunities.all? { |o| o.is_a? Opportunity }).to eq true
284
284
  expect(account.owner).to be_a Owner