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
@@ -7,18 +7,22 @@ module ActiveForce
7
7
  @klass = klass.to_s
8
8
  end
9
9
 
10
+ def table_name name = nil
11
+ @name = name || @name || pick_table_name
12
+ end
13
+
10
14
  def name
11
15
  @name ||= pick_table_name
12
16
  end
13
17
 
14
- def custom_table_name?
18
+ def custom_table?
15
19
  !StandardTypes::STANDARD_TYPES.include?(name_without_namespace)
16
20
  end
17
21
 
18
22
  private
19
23
 
20
24
  def pick_table_name
21
- if custom_table_name?
25
+ if custom_table?
22
26
  "#{ name_without_namespace }__c"
23
27
  else
24
28
  name_without_namespace
@@ -1,3 +1,3 @@
1
1
  module ActiveForce
2
- VERSION = "0.6.1"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -18,6 +18,7 @@ module ActiveForce
18
18
  @attributes ||= sfdc_columns.map do |column|
19
19
  Attribute.new column_to_field(column), column
20
20
  end
21
+ @attributes - [:id]
21
22
  end
22
23
 
23
24
  def sfdc_columns
@@ -1,5 +1,3 @@
1
- require 'active_force/sobject'
2
-
3
1
  class <%= @class_name %> < ActiveForce::SObject
4
2
  <% attributes.each do |attribute| -%>
5
3
  <%= attribute_line attribute %>
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'active_force/active_query'
3
2
 
4
3
  describe ActiveForce::ActiveQuery do
5
4
  let(:sobject) do
@@ -44,17 +43,22 @@ describe ActiveForce::ActiveQuery do
44
43
  describe "condition mapping" do
45
44
  it "maps conditions for a .where" do
46
45
  active_query.where(field: 123)
47
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = 123")
46
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
47
+ end
48
+
49
+ it 'transforms an array to a WHERE/IN clause' do
50
+ active_query.where(field: ['foo', 'bar'])
51
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c IN ('foo','bar'))")
48
52
  end
49
53
 
50
54
  it "encloses the value in quotes if it's a string" do
51
55
  active_query.where field: "hello"
52
- expect(active_query.to_s).to end_with("Field__c = 'hello'")
56
+ expect(active_query.to_s).to end_with("(Field__c = 'hello')")
53
57
  end
54
58
 
55
59
  it "puts NULL when a field is set as nil" do
56
60
  active_query.where field: nil
57
- expect(active_query.to_s).to end_with("Field__c = NULL")
61
+ expect(active_query.to_s).to end_with("(Field__c = NULL)")
58
62
  end
59
63
 
60
64
  describe 'bind parameters' do
@@ -67,17 +71,17 @@ describe ActiveForce::ActiveQuery do
67
71
 
68
72
  it 'accepts bind parameters' do
69
73
  active_query.where('Field__c = ?', 123)
70
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = 123")
74
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
71
75
  end
72
76
 
73
77
  it 'accepts nil bind parameters' do
74
78
  active_query.where('Field__c = ?', nil)
75
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = NULL")
79
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = NULL)")
76
80
  end
77
81
 
78
82
  it 'accepts multiple bind parameters' do
79
83
  active_query.where('Field__c = ? AND Other_Field__c = ? AND Name = ?', 123, 321, 'Bob')
80
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob'")
84
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
81
85
  end
82
86
 
83
87
  it 'complains when there given an incorrect number of bind parameters' do
@@ -89,22 +93,22 @@ describe ActiveForce::ActiveQuery do
89
93
  context 'named bind parameters' do
90
94
  it 'accepts bind parameters' do
91
95
  active_query.where('Field__c = :field', field: 123)
92
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = 123")
96
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
93
97
  end
94
98
 
95
99
  it 'accepts nil bind parameters' do
96
100
  active_query.where('Field__c = :field', field: nil)
97
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = NULL")
101
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = NULL)")
98
102
  end
99
103
 
100
104
  it 'accepts multiple bind parameters' do
101
105
  active_query.where('Field__c = :field AND Other_Field__c = :other_field AND Name = :name', field: 123, other_field: 321, name: 'Bob')
102
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob'")
106
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
103
107
  end
104
108
 
105
109
  it 'accepts multiple bind parameters orderless' do
106
110
  active_query.where('Field__c = :field AND Other_Field__c = :other_field AND Name = :name', name: 'Bob', other_field: 321, field: 123)
107
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob'")
111
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
108
112
  end
109
113
 
110
114
  it 'complains when there given an incorrect number of bind parameters' do
@@ -120,7 +124,7 @@ describe ActiveForce::ActiveQuery do
120
124
  it "should query the client, with the SFDC field names and correctly enclosed values" do
121
125
  expect(client).to receive :query
122
126
  active_query.find_by field: 123
123
- expect(active_query.to_s).to eq "SELECT Id FROM table_name WHERE Field__c = 123 LIMIT 1"
127
+ expect(active_query.to_s).to eq "SELECT Id FROM table_name WHERE (Field__c = 123) LIMIT 1"
124
128
  end
125
129
  end
126
130
 
@@ -137,4 +141,27 @@ describe ActiveForce::ActiveQuery do
137
141
  active_query.map {}
138
142
  end
139
143
  end
144
+
145
+ describe "prevent SOQL injection attacks" do
146
+ let(:mappings){ { quote_field: "QuoteField", backslash_field: "Backslash_Field__c", number_field: "NumberField" } }
147
+ let(:quote_input){ "' OR Id!=NULL OR Id='" }
148
+ let(:backslash_input){ "\\" }
149
+ let(:number_input){ 123 }
150
+ let(:expected_query){ "SELECT Id FROM table_name WHERE (Backslash_Field__c = '\\\\' AND NumberField = 123 AND QuoteField = ''' OR Id!=NULL OR Id=''')" }
151
+
152
+ it 'escapes quotes and backslashes in bind parameters' do
153
+ active_query.where('Backslash_Field__c = :backslash_field AND NumberField = :number_field AND QuoteField = :quote_field', number_field: number_input, backslash_field: backslash_input, quote_field: quote_input)
154
+ expect(active_query.to_s).to eq(expected_query)
155
+ end
156
+
157
+ it 'escapes quotes and backslashes in named bind parameters' do
158
+ active_query.where('Backslash_Field__c = ? AND NumberField = ? AND QuoteField = ?', backslash_input, number_input, quote_input)
159
+ expect(active_query.to_s).to eq(expected_query)
160
+ end
161
+
162
+ it 'escapes quotes and backslashes in hash conditions' do
163
+ active_query.where(backslash_field: backslash_input, number_field: number_input, quote_field: quote_input)
164
+ expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Backslash_Field__c = '\\\\') AND (NumberField = 123) AND (QuoteField = ''' OR Id!=NULL OR Id=''')")
165
+ end
166
+ end
140
167
  end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ module ActiveForce
4
+ module Association
5
+ describe RelationModelBuilder do
6
+ let(:instance){ described_class.new association, value }
7
+
8
+ describe '#build_relation_model' do
9
+ context 'has_many' do
10
+ let(:association){ HasManyAssociation.new Post, :comments }
11
+
12
+ context 'with values' do
13
+ let(:value) do
14
+ build_restforce_collection([
15
+ Restforce::SObject.new({'Id' => '213', 'PostId' => '123'}),
16
+ Restforce::SObject.new({'Id' => '214', 'PostId' => '123'})
17
+ ])
18
+ end
19
+
20
+ it 'returns an array of Comments' do
21
+ comments = instance.build_relation_model
22
+ expect(comments).to be_a Array
23
+ expect(comments.all?{ |c| c.is_a? Comment }).to be true
24
+ end
25
+ end
26
+
27
+ context 'without values' do
28
+ let(:value){ nil }
29
+
30
+ it 'returns an empty array' do
31
+ comments = instance.build_relation_model
32
+ expect(comments).to be_a Array
33
+ expect(comments).to be_empty
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'belongs_to' do
39
+ let(:association){ BelongsToAssociation.new(Comment, :post) }
40
+
41
+ context 'with a value' do
42
+ let(:value) do
43
+ build_restforce_sobject 'Id' => '213'
44
+ end
45
+
46
+ it 'returns a post' do
47
+ expect(instance.build_relation_model).to be_a Post
48
+ end
49
+ end
50
+
51
+ context 'without a value' do
52
+ let(:value){ nil }
53
+
54
+ it 'returns nil' do
55
+ expect(instance.build_relation_model).to be_nil
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,10 +1,8 @@
1
1
  require 'spec_helper'
2
- require 'active_force/association'
3
2
 
4
3
  describe ActiveForce::SObject do
5
-
6
4
  let :post do
7
- Post.new(id: "1")
5
+ Post.new(id: "1", title: 'Ham')
8
6
  end
9
7
 
10
8
  let :comment do
@@ -12,29 +10,14 @@ describe ActiveForce::SObject do
12
10
  end
13
11
 
14
12
  let :client do
15
- double("sfdc_client", query: [Restforce::Mash.new(id: 1)])
13
+ double("sfdc_client", query: [Restforce::Mash.new("Id" => 1)])
16
14
  end
17
15
 
18
16
  before do
19
- class Post < ActiveForce::SObject
20
- self.table_name = "Post__c"
21
- end
22
-
23
- class Comment < ActiveForce::SObject
24
- field :post_id, from: "PostId"
25
- self.table_name = "Comment__c"
26
- end
27
-
28
17
  allow(ActiveForce::SObject).to receive(:sfdc_client).and_return client
29
18
  end
30
19
 
31
20
  describe "has_many_query" do
32
- before do
33
- class Post < ActiveForce::SObject
34
- has_many :comments
35
- end
36
- end
37
-
38
21
  it "should respond to relation method" do
39
22
  expect(post).to respond_to(:comments)
40
23
  end
@@ -51,7 +34,7 @@ describe ActiveForce::SObject do
51
34
 
52
35
  describe 'to_s' do
53
36
  it "should return a SOQL statment" do
54
- soql = "SELECT Id, PostId FROM Comment__c WHERE PostId = '1'"
37
+ soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (PostId = '1')"
55
38
  expect(post.comments.to_s).to eq soql
56
39
  end
57
40
  end
@@ -59,80 +42,62 @@ describe ActiveForce::SObject do
59
42
  context 'when the SObject is namespaced' do
60
43
  let(:account){ Foo::Account.new(id: '1') }
61
44
 
62
- before do
63
- module Foo
64
- class Opportunity < ActiveForce::SObject
65
- field :account_id, from: 'AccountId'
66
- end
67
-
68
- class Account < ActiveForce::SObject
69
- has_many :opportunities, model: Foo::Opportunity
70
- end
71
- end
72
- end
73
-
74
45
  it 'correctly infers the foreign key and forms the correct query' do
75
- soql = "SELECT Id, AccountId FROM Opportunity WHERE AccountId = '1'"
46
+ soql = "SELECT Id, AccountId, Partner_Account_Id__c FROM Opportunity WHERE (AccountId = '1')"
76
47
  expect(account.opportunities.to_s).to eq soql
77
48
  end
78
49
 
79
50
  it 'uses an explicit foreign key if it is supplied' do
80
- Foo::Opportunity.field :partner_account_id, from: 'Partner_Account_Id__c'
81
- Foo::Account.has_many :opportunities, foreign_key: :partner_account_id, model: Foo::Opportunity
82
- soql = "SELECT Id, AccountId, Partner_Account_Id__c FROM Opportunity WHERE Partner_Account_Id__c = '1'"
83
- expect(account.opportunities.to_s).to eq soql
51
+ soql = "SELECT Id, AccountId, Partner_Account_Id__c FROM Opportunity WHERE (Partner_Account_Id__c = '1')"
52
+ expect(account.partner_opportunities.to_s).to eq soql
84
53
  end
85
54
  end
86
55
  end
87
56
 
88
57
  describe 'has_many(options)' do
89
- before do
90
- Post.has_many :comments
91
- end
92
-
93
58
  it 'should allow to send a different query table name' do
94
- Post.has_many :ugly_comments, { model: Comment }
95
- soql = "SELECT Id, PostId FROM Comment__c WHERE PostId = '1'"
59
+ soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (PostId = '1')"
96
60
  expect(post.ugly_comments.to_s).to eq soql
97
61
  end
98
62
 
99
63
  it 'should allow to change the foreign key' do
100
- Post.has_many :comments, { foreign_key: :poster }
101
- Comment.field :poster, from: 'PostId'
102
- soql = "SELECT Id, PostId FROM Comment__c WHERE PostId = '1'"
103
- expect(post.comments.to_s).to eq soql
64
+ soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (PosterId__c = '1')"
65
+ expect(post.poster_comments.to_s).to eq soql
104
66
  end
105
67
 
106
68
  it 'should allow to add a where condition' do
107
- Post.has_many :comments, { where: '1 = 1' }
108
- soql = "SELECT Id, PostId FROM Comment__c WHERE 1 = 1 AND PostId = '1'"
109
- expect(post.comments.to_s).to eq soql
69
+ soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (1 = 0) AND (PostId = '1')"
70
+ expect(post.impossible_comments.to_s).to eq soql
71
+ end
72
+
73
+ it 'accepts custom scoping' do
74
+ soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (Body__c = 'RE: Ham') AND (PostId = '1') ORDER BY CreationDate DESC"
75
+ expect(post.reply_comments.to_s).to eq soql
76
+ end
77
+
78
+ it 'accepts custom scoping that preloads associations of the association' do
79
+ account = Salesforce::Account.new id: '1', business_partner: 'qwerty'
80
+ soql = "SELECT Id, OwnerId, AccountId, Business_Partner__c, Owner.Id FROM Opportunity WHERE (Business_Partner__c = 'qwerty') AND (AccountId = '1')"
81
+ expect(account.partner_opportunities.to_s).to eq soql
110
82
  end
111
83
 
112
84
  it 'should use a convention name for the foreign key' do
113
- soql = "SELECT Id, PostId FROM Comment__c WHERE PostId = '1'"
85
+ soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (PostId = '1')"
114
86
  expect(post.comments.to_s).to eq soql
115
87
  end
116
-
117
88
  end
118
89
 
119
90
  describe "belongs_to" do
120
- before do
121
- Comment.belongs_to :post
122
- end
123
-
124
91
  it "should get the resource it belongs to" do
125
92
  expect(comment.post).to be_instance_of(Post)
126
93
  end
127
94
 
128
95
  it "should allow to pass a foreign key as options" do
129
- class Comment < ActiveForce::SObject
130
- field :fancy_post_id, from: 'PostId'
131
- belongs_to :post, foreign_key: :fancy_post_id
132
- end
96
+ Comment.belongs_to :post, foreign_key: :fancy_post_id
133
97
  allow(comment).to receive(:fancy_post_id).and_return "2"
134
- expect(client).to receive(:query).with("SELECT Id FROM Post__c WHERE Id = '2' LIMIT 1")
98
+ expect(client).to receive(:query).with("SELECT Id, Title__c FROM Post__c WHERE (Id = '2') LIMIT 1")
135
99
  comment.post
100
+ Comment.belongs_to :post # reset association to original value
136
101
  end
137
102
 
138
103
  it 'makes only one API call to fetch the associated object' do
@@ -141,29 +106,37 @@ describe ActiveForce::SObject do
141
106
  comment.post
142
107
  end
143
108
 
144
- it 'accepts assignment of an existing object as an association' do
145
- expect(client).to_not receive(:query)
146
- other_post = Post.new(id: "2")
147
- comment.post = other_post
148
- expect(comment.post_id).to eq other_post.id
149
- expect(comment.post).to eq other_post
150
- end
109
+ describe "assignments" do
110
+ let(:comment) do
111
+ comment = Comment.new(id: '1')
112
+ comment.post = Post.new(id: '1')
113
+ comment
114
+ end
151
115
 
152
- context 'when the SObject is namespaced' do
153
- let(:attachment){ Foo::Attachment.new(id: '1', lead_id: '2') }
154
116
  before do
155
- module Foo
156
- class Lead < ActiveForce::SObject; end
117
+ expect(client).to_not receive(:query)
118
+ end
119
+
120
+ it 'accepts assignment of an existing object as an association' do
121
+ expect(client).to_not receive(:query)
122
+ other_post = Post.new(id: "2")
123
+ comment.post = other_post
124
+ expect(comment.post_id).to eq other_post.id
125
+ expect(comment.post).to eq other_post
126
+ end
157
127
 
158
- class Attachment < ActiveForce::SObject
159
- field :lead_id, from: 'Lead_Id__c'
160
- belongs_to :lead, model: Foo::Lead
161
- end
162
- end
128
+ it 'can desassociate an object by setting it as nil' do
129
+ comment.post = nil
130
+ expect(comment.post_id).to eq nil
131
+ expect(comment.post).to eq nil
163
132
  end
133
+ end
134
+
135
+ context 'when the SObject is namespaced' do
136
+ let(:attachment){ Foo::Attachment.new(id: '1', lead_id: '2') }
164
137
 
165
138
  it 'generates the correct query' do
166
- expect(client).to receive(:query).with("SELECT Id FROM Lead WHERE Id = '2' LIMIT 1")
139
+ expect(client).to receive(:query).with("SELECT Id FROM Lead WHERE (Id = '2') LIMIT 1")
167
140
  attachment.lead
168
141
  end
169
142
 
@@ -173,18 +146,10 @@ describe ActiveForce::SObject do
173
146
 
174
147
  context 'when given a foreign key' do
175
148
  let(:attachment){ Foo::Attachment.new(id: '1', fancy_lead_id: '2') }
176
- before do
177
- module Foo
178
- class Attachment < ActiveForce::SObject
179
- field :fancy_lead_id, from: 'LeadId'
180
- belongs_to :lead, model: Foo::Lead, foreign_key: :fancy_lead_id
181
- end
182
- end
183
- end
184
149
 
185
150
  it 'generates the correct query' do
186
- expect(client).to receive(:query).with("SELECT Id FROM Lead WHERE Id = '2' LIMIT 1")
187
- attachment.lead
151
+ expect(client).to receive(:query).with("SELECT Id FROM Lead WHERE (Id = '2') LIMIT 1")
152
+ attachment.fancy_lead
188
153
  end
189
154
  end
190
155
  end