active_force 0.7.1 → 0.15.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 (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