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
@@ -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