active_force 0.16.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7691f9f6f82d893a6aafdf29cfc6228ab2fe31988fafcf5be95070347a0286de
4
- data.tar.gz: 4d0da25e5e12f9014407dba1871953a0b05f58227188d53130eba27dd537aa4d
3
+ metadata.gz: 6de1df55a3215360cd78f76fa3f55d05ca9e8f9f9603d5ab4257667a5fd961a5
4
+ data.tar.gz: e85f2ae5bd075070adc261ec3a6d566eea2f9cf19c4d4423fa1b859cafdabcc4
5
5
  SHA512:
6
- metadata.gz: 2e8cf96c34eb81e3e4236455538bc462de07d02f7149ab064125d4c86f52a96c7e61e98ff91b9fe48e3b5b6d79d88ded4bb47966800f021e153a970adac802b4
7
- data.tar.gz: 6b7edaa7cf4161921b1d42c3a26fe896781bd3198ce981b2693852f3fe3ff6f1690e27e61ec781761fbe06d10634fc720013f1b2f3a266d3de6a189dd37af2a5
6
+ metadata.gz: 5268395d3fcc92a705b909026b6de6deb36b4c6a9b622d0ee63307ed08d16142e96abca2bafb902c55fca02955d75c34aa3348eb84fc14c47784d3548b492f89
7
+ data.tar.gz: 3ea676afd55bb62601e211250c487a86e7902f1dee1f8b6fdc41410b903ce2052e5a4f893cc869798ba3bf5c1270aad2c84ea21c24dd137909e494e7c0564ff9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Not released
4
4
 
5
+ ## 0.18.0
6
+
7
+ - Fix eager loading of scoped associations. (https://github.com/Beyond-Finance/active_force/pull/67)
8
+ - Adding `.blank?`, `.present?`, and `.any?` delegators to `ActiveQuery`. (https://github.com/Beyond-Finance/active_force/pull/68)
9
+ - Adding `update` and `update!` class methods on `SObject`. (https://github.com/Beyond-Finance/active_force/pull/66)
10
+ - Allow an argument to `last` allowing the query to select the `last(n)` records. Default is 1. (https://github.com/Beyond-Finance/active_force/pull/66)
11
+
12
+ ## 0.17.0
13
+
14
+ - Fix bug with has_many queries due to query method chaining mutating in-place (https://github.com/Beyond-Finance/active_force/pull/10)
15
+
5
16
  ## 0.16.0
6
17
 
7
18
  - Fix `default` in models when default value is overridden by the same value, it is still sent to salesforce (https://github.com/Beyond-Finance/active_force/pull/61)
@@ -21,9 +21,9 @@ module ActiveForce
21
21
  attr_reader :sobject, :association_mapping, :belongs_to_association_mapping
22
22
 
23
23
  def_delegators :sobject, :sfdc_client, :build, :table_name, :mappings
24
- def_delegators :to_a, :each, :map, :inspect, :pluck, :each_with_object
24
+ def_delegators :to_a, :blank?, :present?, :any?, :each, :map, :inspect, :pluck, :each_with_object
25
25
 
26
- def initialize (sobject, custom_table_name = nil)
26
+ def initialize(sobject, custom_table_name = nil)
27
27
  @sobject = sobject
28
28
  @association_mapping = {}
29
29
  @belongs_to_association_mapping = {}
@@ -42,36 +42,34 @@ module ActiveForce
42
42
  alias_method :all, :to_a
43
43
 
44
44
  def count
45
- super
46
- sfdc_client.query(to_s).first.expr0
45
+ sfdc_client.query(super.to_s).first.expr0
47
46
  end
48
47
 
49
48
  def sum field
50
- super(mappings[field])
51
- sfdc_client.query(to_s).first.expr0
49
+ raise ArgumentError, 'field is required' if field.blank?
50
+ raise ArgumentError, "field '#{field}' does not exist on #{sobject}" unless mappings.key?(field.to_sym)
51
+
52
+ sfdc_client.query(super(mappings.fetch(field.to_sym)).to_s).first.expr0
52
53
  end
53
54
 
54
55
  def limit limit
55
- super
56
- limit == 1 ? to_a.first : self
56
+ limit == 1 ? super.to_a.first : super
57
57
  end
58
58
 
59
59
  def not args=nil, *rest
60
60
  return self if args.nil?
61
+
61
62
  super build_condition args, rest
62
- self
63
63
  end
64
64
 
65
65
  def where args=nil, *rest
66
66
  return self if args.nil?
67
- return clone_self_and_clear_cache.where(args, *rest) if @decorated_records.present?
68
67
  super build_condition args, rest
69
- self
70
68
  end
71
69
 
72
- def select *fields
73
- fields.map! { |field| mappings[field] }
74
- super *fields
70
+ def select *selected_fields
71
+ selected_fields.map! { |field| mappings[field] }
72
+ super *selected_fields
75
73
  end
76
74
 
77
75
  def find!(id)
@@ -100,8 +98,10 @@ module ActiveForce
100
98
  end
101
99
 
102
100
  def none
103
- @records = []
104
- where(id: '1'*18).where(id: '0'*18)
101
+ clone_and_set_instance_variables(
102
+ records: [],
103
+ conditions: [build_condition(id: '1' * 18), build_condition(id: '0' * 18)]
104
+ )
105
105
  end
106
106
 
107
107
  def loaded?
@@ -205,13 +205,6 @@ module ActiveForce
205
205
  sfdc_client.query(self.to_s)
206
206
  end
207
207
 
208
- def clone_self_and_clear_cache
209
- new_query = self.clone
210
- new_query.instance_variable_set(:@decorated_records, nil)
211
- new_query.instance_variable_set(:@records, nil)
212
- new_query
213
- end
214
-
215
208
  def build_order_by(args)
216
209
  args.map do |arg|
217
210
  case arg
@@ -228,6 +221,5 @@ module ActiveForce
228
221
  def order_type(type)
229
222
  type == :desc ? 'DESC' : 'ASC'
230
223
  end
231
-
232
224
  end
233
225
  end
@@ -26,6 +26,14 @@ module ActiveForce
26
26
  options[:relationship_name] || relation_model.to_s.constantize.table_name
27
27
  end
28
28
 
29
+ def scoped_as
30
+ options[:scoped_as] || nil
31
+ end
32
+
33
+ def scoped?
34
+ options[:scoped_as].present?
35
+ end
36
+
29
37
  ###
30
38
  # Does this association's relation_model represent
31
39
  # +sfdc_table_name+? Examples of +sfdc_table_name+
@@ -1,5 +1,6 @@
1
1
  module ActiveForce
2
2
  module Association
3
+ class InvalidEagerLoadAssociation < StandardError; end
3
4
  class EagerLoadProjectionBuilder
4
5
  class << self
5
6
  def build(association, parent_association_field = nil)
@@ -34,6 +35,13 @@ module ActiveForce
34
35
  def projections
35
36
  raise "Must define #{self.class.name}#projections"
36
37
  end
38
+
39
+ def apply_association_scope(query)
40
+ return query unless association.scoped?
41
+ raise InvalidEagerLoadAssociation, "Cannot use scopes that expect arguments: #{association.relation_name}" if association.scoped_as.arity.positive?
42
+
43
+ query.instance_exec(&association.scoped_as)
44
+ end
37
45
  end
38
46
 
39
47
  class HasManyAssociationProjectionBuilder < AbstractProjectionBuilder
@@ -43,17 +51,17 @@ module ActiveForce
43
51
  # to be pluralized
44
52
  def projections
45
53
  relationship_name = association.sfdc_association_field
46
- query = Query.new relationship_name
54
+ query = ActiveQuery.new(association.relation_model, relationship_name)
47
55
  query.fields association.relation_model.fields
48
- ["(#{query.to_s})"]
56
+ ["(#{apply_association_scope(query).to_s})"]
49
57
  end
50
58
  end
51
59
 
52
60
  class HasOneAssociationProjectionBuilder < AbstractProjectionBuilder
53
61
  def projections
54
- query = Query.new association.sfdc_association_field
62
+ query = ActiveQuery.new(association.relation_model, association.sfdc_association_field)
55
63
  query.fields association.relation_model.fields
56
- ["(#{query.to_s})"]
64
+ ["(#{apply_association_scope(query).to_s})"]
57
65
  end
58
66
  end
59
67
 
@@ -31,33 +31,34 @@ module ActiveForce
31
31
  end
32
32
 
33
33
  def select *columns
34
- @query_fields = columns
35
- self
34
+ clone_and_set_instance_variables(query_fields: columns)
35
+ end
36
+
37
+ def where condition = nil
38
+ new_conditions = @conditions | [condition]
39
+ if new_conditions != @conditions
40
+ clone_and_set_instance_variables({conditions: new_conditions})
41
+ else
42
+ self
43
+ end
36
44
  end
37
45
 
38
46
  def not condition
39
- @conditions << "NOT ((#{ condition.join(') AND (') }))"
40
- self
47
+ condition ? where("NOT ((#{condition.join(') AND (')}))") : self
41
48
  end
42
49
 
43
50
  def or query
44
- @conditions = ["(#{ and_conditions }) OR (#{ query.and_conditions })"]
45
- self
46
- end
51
+ return self unless query
47
52
 
48
- def where condition = nil
49
- @conditions << condition if condition
50
- self
53
+ clone_and_set_instance_variables(conditions: ["(#{and_conditions}) OR (#{query.and_conditions})"])
51
54
  end
52
55
 
53
56
  def order order
54
- @order = order if order
55
- self
57
+ order ? clone_and_set_instance_variables(order: order) : self
56
58
  end
57
59
 
58
60
  def limit size
59
- @size = size if size
60
- self
61
+ size ? clone_and_set_instance_variables(size: size) : self
61
62
  end
62
63
 
63
64
  def limit_value
@@ -65,8 +66,7 @@ module ActiveForce
65
66
  end
66
67
 
67
68
  def offset offset
68
- @offset = offset
69
- self
69
+ clone_and_set_instance_variables(offset: offset)
70
70
  end
71
71
 
72
72
  def offset_value
@@ -74,31 +74,29 @@ module ActiveForce
74
74
  end
75
75
 
76
76
  def find id
77
- where "#{ @table_id } = '#{ id }'"
78
- limit 1
77
+ where("#{ @table_id } = '#{ id }'").limit 1
79
78
  end
80
79
 
81
80
  def first
82
81
  limit 1
83
82
  end
84
83
 
85
- def last
86
- order("Id DESC").limit(1)
84
+ def last(limit = 1)
85
+ order("Id DESC").limit(limit)
87
86
  end
88
87
 
89
88
  def join object_query
90
- fields ["(#{ object_query.to_s })"]
91
- self
89
+ chained_query = self.clone
90
+ chained_query.fields ["(#{ object_query.to_s })"]
91
+ chained_query
92
92
  end
93
93
 
94
94
  def count
95
- @query_fields = ["count(Id)"]
96
- self
95
+ clone_and_set_instance_variables(query_fields: ["count(Id)"])
97
96
  end
98
97
 
99
98
  def sum field
100
- @query_fields = ["sum(#{field})"]
101
- self
99
+ clone_and_set_instance_variables(query_fields: ["sum(#{field})"])
102
100
  end
103
101
 
104
102
  protected
@@ -125,5 +123,13 @@ module ActiveForce
125
123
  def build_offset
126
124
  "OFFSET #{ @offset }" if @offset
127
125
  end
126
+
127
+ def clone_and_set_instance_variables instance_variable_hash={}
128
+ clone = self.clone
129
+ clone.instance_variable_set(:@decorated_records, nil)
130
+ clone.instance_variable_set(:@records, nil)
131
+ instance_variable_hash.each { |k,v| clone.instance_variable_set("@#{k.to_s}", v) }
132
+ clone
133
+ end
128
134
  end
129
135
  end
@@ -129,6 +129,14 @@ module ActiveForce
129
129
  new(args).create!
130
130
  end
131
131
 
132
+ def self.update(id, attributes)
133
+ new(attributes.merge(id: id)).update
134
+ end
135
+
136
+ def self.update!(id, attributes)
137
+ new(attributes.merge(id: id)).update!
138
+ end
139
+
132
140
  def save!
133
141
  run_callbacks :save do
134
142
  if persisted?
@@ -231,7 +239,7 @@ module ActiveForce
231
239
  end
232
240
 
233
241
  def default_attributes
234
- @attributes.each_value.select do |value|
242
+ @attributes.each_value.select do |value|
235
243
  value.is_a?(ActiveModel::Attribute::UserProvidedDefault) || value.instance_values["original_attribute"].is_a?(ActiveModel::Attribute::UserProvidedDefault)
236
244
  end.map(&:name)
237
245
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveForce
4
- VERSION = '0.16.0'
4
+ VERSION = '0.18.0'
5
5
  end
@@ -9,7 +9,7 @@ describe ActiveForce::ActiveQuery do
9
9
  })
10
10
  end
11
11
  let(:mappings){ { id: "Id", field: "Field__c", other_field: "Other_Field" } }
12
- let(:client){ double("client") }
12
+ let(:client) { double('client', query: nil) }
13
13
  let(:active_query){ described_class.new(sobject) }
14
14
  let(:api_result) do
15
15
  [
@@ -18,7 +18,6 @@ describe ActiveForce::ActiveQuery do
18
18
  ]
19
19
  end
20
20
 
21
-
22
21
  before do
23
22
  allow(active_query).to receive(:sfdc_client).and_return client
24
23
  allow(active_query).to receive(:build).and_return Object.new
@@ -40,50 +39,113 @@ describe ActiveForce::ActiveQuery do
40
39
  end
41
40
  end
42
41
 
42
+ describe '#blank? delegation' do
43
+ before do
44
+ allow(client).to receive(:query).and_return(api_result)
45
+ end
46
+
47
+ context 'when there are no records' do
48
+ let(:api_result) { [] }
49
+
50
+ it 'returns true' do
51
+ result = active_query.where("Text_Label = 'foo'").blank?
52
+ expect(result).to be true
53
+ end
54
+ end
55
+
56
+ context 'when records are returned' do
57
+ it 'returns false' do
58
+ result = active_query.where("Text_Label = 'foo'").blank?
59
+ expect(result).to be false
60
+ end
61
+ end
62
+ end
63
+
64
+ describe '#present? delegation' do
65
+ before do
66
+ allow(client).to receive(:query).and_return(api_result)
67
+ end
68
+
69
+ context 'when there are no records' do
70
+ let(:api_result) { [] }
71
+
72
+ it 'returns false' do
73
+ result = active_query.where("Text_Label = 'foo'").present?
74
+ expect(result).to be false
75
+ end
76
+ end
77
+
78
+ context 'when there are records' do
79
+ it 'returns true' do
80
+ result = active_query.where("Text_Label = 'foo'").present?
81
+ expect(result).to be true
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#any? delegation' do
87
+ before do
88
+ allow(client).to receive(:query).and_return(api_result)
89
+ end
90
+
91
+ context 'when there are no records' do
92
+ let(:api_result) { [] }
93
+
94
+ it 'returns true' do
95
+ result = active_query.where("Text_Label = 'foo'").any?
96
+ expect(result).to be false
97
+ end
98
+ end
99
+
100
+ context 'when records are returned' do
101
+ it 'returns false' do
102
+ result = active_query.where("Text_Label = 'foo'").any?
103
+ expect(result).to be true
104
+ end
105
+ end
106
+ end
107
+
43
108
  describe "select only some field using mappings" do
44
109
  it "should return a query only with selected field" do
45
- active_query.select(:field)
46
- expect(active_query.to_s).to eq("SELECT Field__c FROM table_name")
110
+ new_query = active_query.select(:field)
111
+ expect(new_query.to_s).to eq("SELECT Field__c FROM table_name")
47
112
  end
48
113
  end
49
114
 
50
115
  describe "condition mapping" do
51
116
  it "maps conditions for a .where" do
52
- active_query.where(field: 123)
53
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
117
+ new_query = active_query.where(field: 123)
118
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
54
119
  end
55
120
 
56
121
  it 'transforms an array to a WHERE/IN clause' do
57
- active_query.where(field: ['foo', 'bar'])
58
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c IN ('foo','bar'))")
122
+ new_query = active_query.where(field: ['foo', 'bar'])
123
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c IN ('foo','bar'))")
59
124
  end
60
125
 
61
126
  it "encloses the value in quotes if it's a string" do
62
- active_query.where field: "hello"
63
- expect(active_query.to_s).to end_with("(Field__c = 'hello')")
127
+ new_query = active_query.where field: "hello"
128
+ expect(new_query.to_s).to end_with("(Field__c = 'hello')")
64
129
  end
65
130
 
66
131
  it "formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if it's a DateTime" do
67
132
  value = DateTime.now
68
- active_query.where(field: value)
69
- expect(active_query.to_s).to end_with("(Field__c = #{value.iso8601})")
133
+ expect(active_query.where(field: value).to_s).to end_with("(Field__c = #{value.iso8601})")
70
134
  end
71
135
 
72
136
  it "formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if it's a Time" do
73
137
  value = Time.now
74
- active_query.where(field: value)
75
- expect(active_query.to_s).to end_with("(Field__c = #{value.iso8601})")
138
+ expect(active_query.where(field: value).to_s).to end_with("(Field__c = #{value.iso8601})")
76
139
  end
77
140
 
78
141
  it "formats as YYYY-MM-DD and does not enclose in quotes if it's a Date" do
79
142
  value = Date.today
80
- active_query.where(field: value)
81
- expect(active_query.to_s).to end_with("(Field__c = #{value.iso8601})")
143
+ expect(active_query.where(field: value).to_s).to end_with("(Field__c = #{value.iso8601})")
82
144
  end
83
145
 
84
146
  it "puts NULL when a field is set as nil" do
85
- active_query.where field: nil
86
- expect(active_query.to_s).to end_with("(Field__c = NULL)")
147
+ new_query = active_query.where field: nil
148
+ expect(new_query.to_s).to end_with("(Field__c = NULL)")
87
149
  end
88
150
 
89
151
  describe 'bind parameters' do
@@ -95,36 +157,33 @@ describe ActiveForce::ActiveQuery do
95
157
  end
96
158
 
97
159
  it 'accepts bind parameters' do
98
- active_query.where('Field__c = ?', 123)
99
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
160
+ new_query = active_query.where('Field__c = ?', 123)
161
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
100
162
  end
101
163
 
102
164
  it 'accepts nil bind parameters' do
103
- active_query.where('Field__c = ?', nil)
104
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = NULL)")
165
+ new_query = active_query.where('Field__c = ?', nil)
166
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = NULL)")
105
167
  end
106
168
 
107
169
  it 'accepts multiple bind parameters' do
108
- active_query.where('Field__c = ? AND Other_Field__c = ? AND Name = ?', 123, 321, 'Bob')
109
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
170
+ new_query = active_query.where('Field__c = ? AND Other_Field__c = ? AND Name = ?', 123, 321, 'Bob')
171
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
110
172
  end
111
173
 
112
174
  it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a DateTime' do
113
175
  value = DateTime.now
114
- active_query.where('Field__c > ?', value)
115
- expect(active_query.to_s).to end_with("(Field__c > #{value.iso8601})")
176
+ expect(active_query.where('Field__c > ?', value).to_s).to end_with("(Field__c > #{value.iso8601})")
116
177
  end
117
178
 
118
179
  it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a Time' do
119
180
  value = Time.now
120
- active_query.where('Field__c > ?', value)
121
- expect(active_query.to_s).to end_with("(Field__c > #{value.iso8601})")
181
+ expect(active_query.where('Field__c > ?', value).to_s).to end_with("(Field__c > #{value.iso8601})")
122
182
  end
123
183
 
124
184
  it 'formats as YYYY-MM-DD and does not enclose in quotes if value is a Date' do
125
185
  value = Date.today
126
- active_query.where('Field__c > ?', value)
127
- expect(active_query.to_s).to end_with("(Field__c > #{value.iso8601})")
186
+ expect(active_query.where('Field__c > ?', value).to_s).to end_with("(Field__c > #{value.iso8601})")
128
187
  end
129
188
 
130
189
  it 'complains when there given an incorrect number of bind parameters' do
@@ -135,41 +194,41 @@ describe ActiveForce::ActiveQuery do
135
194
 
136
195
  context 'named bind parameters' do
137
196
  it 'accepts bind parameters' do
138
- active_query.where('Field__c = :field', field: 123)
139
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
197
+ new_query = active_query.where('Field__c = :field', field: 123)
198
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123)")
140
199
  end
141
200
 
142
201
  it 'accepts nil bind parameters' do
143
- active_query.where('Field__c = :field', field: nil)
144
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = NULL)")
202
+ new_query = active_query.where('Field__c = :field', field: nil)
203
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = NULL)")
145
204
  end
146
205
 
147
206
  it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a DateTime' do
148
207
  value = DateTime.now
149
- active_query.where('Field__c < :field', field: value)
150
- expect(active_query.to_s).to end_with("(Field__c < #{value.iso8601})")
208
+ new_query = active_query.where('Field__c < :field', field: value)
209
+ expect(new_query.to_s).to end_with("(Field__c < #{value.iso8601})")
151
210
  end
152
211
 
153
212
  it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a Time' do
154
213
  value = Time.now
155
- active_query.where('Field__c < :field', field: value)
156
- expect(active_query.to_s).to end_with("(Field__c < #{value.iso8601})")
214
+ new_query = active_query.where('Field__c < :field', field: value)
215
+ expect(new_query.to_s).to end_with("(Field__c < #{value.iso8601})")
157
216
  end
158
217
 
159
218
  it 'formats as YYYY-MM-DD and does not enclose in quotes if value is a Date' do
160
219
  value = Date.today
161
- active_query.where('Field__c < :field', field: value)
162
- expect(active_query.to_s).to end_with("(Field__c < #{value.iso8601})")
220
+ new_query = active_query.where('Field__c < :field', field: value)
221
+ expect(new_query.to_s).to end_with("(Field__c < #{value.iso8601})")
163
222
  end
164
223
 
165
224
  it 'accepts multiple bind parameters' do
166
- active_query.where('Field__c = :field AND Other_Field__c = :other_field AND Name = :name', field: 123, other_field: 321, name: 'Bob')
167
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
225
+ new_query = active_query.where('Field__c = :field AND Other_Field__c = :other_field AND Name = :name', field: 123, other_field: 321, name: 'Bob')
226
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
168
227
  end
169
228
 
170
229
  it 'accepts multiple bind parameters orderless' do
171
- active_query.where('Field__c = :field AND Other_Field__c = :other_field AND Name = :name', name: 'Bob', other_field: 321, field: 123)
172
- expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
230
+ new_query = active_query.where('Field__c = :field AND Other_Field__c = :other_field AND Name = :name', name: 'Bob', other_field: 321, field: 123)
231
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = 123 AND Other_Field__c = 321 AND Name = 'Bob')")
173
232
  end
174
233
 
175
234
  it 'complains when there given an incorrect number of bind parameters' do
@@ -198,29 +257,77 @@ describe ActiveForce::ActiveQuery do
198
257
  {"Id" => "0000000000EEEEEFFF"}
199
258
  ]
200
259
  end
260
+
201
261
  it 'allows method chaining' do
202
262
  result = active_query.where("Text_Label = 'foo'").where("Checkbox_Label = true")
203
263
  expect(result).to be_a described_class
204
264
  end
205
265
 
266
+ it 'does not execute a query' do
267
+ active_query.where('x')
268
+ expect(client).not_to have_received(:query)
269
+ end
270
+
271
+ context 'when calling `where` on an ActiveQuery object that already has records' do
272
+ context 'after the query result has been decorated' do
273
+ it 'returns a new ActiveQuery object' do
274
+ first_active_query = active_query.where("Text_Label = 'foo'")
275
+ first_active_query.to_a # decorates the results
276
+ second_active_query = first_active_query.where("Checkbox_Label = true")
277
+ second_active_query.to_a
278
+ expect(second_active_query).to be_a described_class
279
+ expect(second_active_query).not_to eq first_active_query
280
+ expect(second_active_query.to_s).not_to eq first_active_query.to_s
281
+ expect(second_active_query.to_a.size).to eq(1)
282
+ end
283
+ end
284
+ end
285
+
206
286
  context 'when calling `where` on an ActiveQuery object that already has records' do
207
- it 'returns a new ActiveQuery object' do
208
- first_active_query = active_query.where("Text_Label = 'foo'")
209
- first_active_query.inspect # so the query is executed
210
- second_active_query = first_active_query.where("Checkbox_Label = true")
211
- second_active_query.inspect
212
- expect(second_active_query).to be_a described_class
213
- expect(second_active_query).not_to eq first_active_query
287
+ context 'without the query result being decorated' do
288
+
289
+ it 'returns a new ActiveQuery object' do
290
+ first_active_query = active_query.where("Text_Label = 'foo'")
291
+ second_active_query = first_active_query.where("Checkbox_Label = true")
292
+ expect(second_active_query).to be_a described_class
293
+ expect(second_active_query).not_to eq first_active_query
294
+ expect(second_active_query.to_s).not_to eq first_active_query.to_s
295
+ expect(second_active_query.to_a.size).to eq(1)
296
+ end
214
297
  end
215
298
  end
299
+ end
216
300
 
301
+ describe '#not' do
302
+ it 'adds a not condition' do
303
+ expect(active_query.not(field: 'x').to_s).to end_with("WHERE (NOT ((Field__c = 'x')))")
304
+ end
305
+
306
+ it 'allows chaining' do
307
+ expect(active_query.where(field: 'x').not(field: 'y').where(field: 'z')).to be_a(described_class)
308
+ end
309
+
310
+ it 'does not mutate the original query' do
311
+ original = active_query.to_s
312
+ active_query.not(field: 'x')
313
+ expect(active_query.to_s).to eq(original)
314
+ end
315
+
316
+ it 'returns the original query if not given a condition' do
317
+ expect(active_query.not).to be(active_query)
318
+ end
319
+
320
+ it 'does not execute a query' do
321
+ active_query.not(field: 'x')
322
+ expect(client).not_to have_received(:query)
323
+ end
217
324
  end
218
325
 
219
326
  describe "#find_by" do
220
327
  it "should query the client, with the SFDC field names and correctly enclosed values" do
221
- expect(client).to receive :query
222
- active_query.find_by field: 123
223
- expect(active_query.to_s).to eq "SELECT Id FROM table_name WHERE (Field__c = 123) LIMIT 1"
328
+ expect(client).to receive(:query).with("SELECT Id FROM table_name WHERE (Field__c = 123) LIMIT 1")
329
+ new_query = active_query.find_by field: 123
330
+ expect(new_query).to be_nil
224
331
  end
225
332
  end
226
333
 
@@ -290,18 +397,18 @@ describe ActiveForce::ActiveQuery do
290
397
  let(:expected_query){ "SELECT Id FROM table_name WHERE (Backslash_Field__c = '\\\\' AND NumberField = 123 AND QuoteField = '\\' OR Id!=NULL OR Id=\\'')" }
291
398
 
292
399
  it 'escapes quotes and backslashes in bind parameters' do
293
- 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)
294
- expect(active_query.to_s).to eq(expected_query)
400
+ new_query = 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)
401
+ expect(new_query.to_s).to eq(expected_query)
295
402
  end
296
403
 
297
404
  it 'escapes quotes and backslashes in named bind parameters' do
298
- active_query.where('Backslash_Field__c = ? AND NumberField = ? AND QuoteField = ?', backslash_input, number_input, quote_input)
299
- expect(active_query.to_s).to eq(expected_query)
405
+ new_query = active_query.where('Backslash_Field__c = ? AND NumberField = ? AND QuoteField = ?', backslash_input, number_input, quote_input)
406
+ expect(new_query.to_s).to eq(expected_query)
300
407
  end
301
408
 
302
409
  it 'escapes quotes and backslashes in hash conditions' do
303
- active_query.where(backslash_field: backslash_input, number_field: number_input, quote_field: quote_input)
304
- 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=\\'')")
410
+ new_query = active_query.where(backslash_field: backslash_input, number_field: number_input, quote_field: quote_input)
411
+ expect(new_query.to_s).to eq("SELECT Id FROM table_name WHERE (Backslash_Field__c = '\\\\') AND (NumberField = 123) AND (QuoteField = '\\' OR Id!=NULL OR Id=\\'')")
305
412
  end
306
413
  end
307
414
 
@@ -40,6 +40,16 @@ describe ActiveForce::SObject do
40
40
  post.comments.to_a
41
41
  end
42
42
 
43
+ it 'is not mutated by #where' do
44
+ post.comments.where(body: 'test').to_a
45
+ expect(post.comments.to_s).to end_with("FROM Comment__c WHERE (PostId = '1')")
46
+ end
47
+
48
+ it 'is not mutated by #none' do
49
+ post.comments.none.to_a
50
+ expect(post.comments.to_s).to end_with("FROM Comment__c WHERE (PostId = '1')")
51
+ end
52
+
43
53
  describe 'to_s' do
44
54
  it "should return a SOQL statment" do
45
55
  soql = "SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__c WHERE (PostId = '1')"
@@ -32,7 +32,7 @@ describe ActiveForce::Query do
32
32
  expect(query.all.to_s).to eq "SELECT Id, name, etc FROM table_name"
33
33
  end
34
34
 
35
- it "should ignore dupicated attributes in select statment" do
35
+ it "should ignore duplicated attributes in select statment" do
36
36
  query.fields ['Id', 'name', 'etc']
37
37
  expect(query.all.to_s).to eq "SELECT Id, name, etc FROM table_name"
38
38
  end
@@ -46,6 +46,19 @@ describe ActiveForce::Query do
46
46
  it "should add multiples conditions to a query with parentheses" do
47
47
  expect(query.where("condition1 = 1").where("condition2 = 2 OR condition3 = 3").to_s).to eq "SELECT Id, name, etc FROM table_name WHERE (condition1 = 1) AND (condition2 = 2 OR condition3 = 3)"
48
48
  end
49
+
50
+ it "should not duplicate conditions" do
51
+ first_query = query.where("name = 'cool'").where("foo = 'baz'")
52
+ second_query = first_query.where("name = 'cool'")
53
+ expect(first_query.to_s).to eq(second_query.to_s)
54
+ expect(first_query.object_id).to eq(second_query.object_id)
55
+ end
56
+
57
+ it "should not update the original query" do
58
+ new_query = query.where("name = 'cool'")
59
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
60
+ expect(new_query.to_s).to eq "SELECT Id, name, etc FROM table_name WHERE (name = 'cool')"
61
+ end
49
62
  end
50
63
 
51
64
  describe ".not" do
@@ -68,12 +81,18 @@ describe ActiveForce::Query do
68
81
  it "should add a limit to a query" do
69
82
  expect(query.limit("25").to_s).to eq "SELECT Id, name, etc FROM table_name LIMIT 25"
70
83
  end
84
+
85
+ it "should not update the original query" do
86
+ new_query = query.limit("25")
87
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
88
+ expect(new_query.to_s).to eq "SELECT Id, name, etc FROM table_name LIMIT 25"
89
+ end
71
90
  end
72
91
 
73
92
  describe ".limit_value" do
74
93
  it "should return the limit value" do
75
- query.limit(4)
76
- expect(query.limit_value).to eq 4
94
+ new_query = query.limit(4)
95
+ expect(new_query.limit_value).to eq 4
77
96
  end
78
97
  end
79
98
 
@@ -81,12 +100,18 @@ describe ActiveForce::Query do
81
100
  it "should add an offset to a query" do
82
101
  expect(query.offset(4).to_s).to eq "SELECT Id, name, etc FROM table_name OFFSET 4"
83
102
  end
103
+
104
+ it "should not update the original query" do
105
+ new_query = query.offset(4)
106
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
107
+ expect(new_query.to_s).to eq "SELECT Id, name, etc FROM table_name OFFSET 4"
108
+ end
84
109
  end
85
110
 
86
111
  describe ".offset_value" do
87
112
  it "should return the offset value" do
88
- query.offset(4)
89
- expect(query.offset_value).to eq 4
113
+ new_query = query.offset(4)
114
+ expect(new_query.offset_value).to eq 4
90
115
  end
91
116
  end
92
117
 
@@ -104,6 +129,12 @@ describe ActiveForce::Query do
104
129
  it "should add a order condition in the statment with WHERE and LIMIT" do
105
130
  expect(query.where("condition1 = 1").order("name desc").limit(1).to_s).to eq "SELECT Id, name, etc FROM table_name WHERE (condition1 = 1) ORDER BY name desc LIMIT 1"
106
131
  end
132
+
133
+ it "should not update the original query" do
134
+ ordered_query = query.order("name desc")
135
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
136
+ expect(ordered_query.to_s).to eq "SELECT Id, name, etc FROM table_name ORDER BY name desc"
137
+ end
107
138
  end
108
139
 
109
140
  describe '.join' do
@@ -116,17 +147,51 @@ describe ActiveForce::Query do
116
147
  it 'should add another select statment on the current select' do
117
148
  expect(query.join(join_query).to_s).to eq 'SELECT Id, name, etc, (SELECT Id, name, etc FROM join_table_name) FROM table_name'
118
149
  end
150
+
151
+ it "should not update the original query" do
152
+ new_query = query.join(join_query)
153
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
154
+ expect(new_query.to_s).to eq 'SELECT Id, name, etc, (SELECT Id, name, etc FROM join_table_name) FROM table_name'
155
+ end
119
156
  end
120
157
 
121
158
  describe '.first' do
122
159
  it 'should return the query for the first record' do
123
160
  expect(query.first.to_s).to eq 'SELECT Id, name, etc FROM table_name LIMIT 1'
124
161
  end
162
+
163
+ it "should not update the original query" do
164
+ new_query = query.first
165
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
166
+ expect(new_query.to_s).to eq 'SELECT Id, name, etc FROM table_name LIMIT 1'
167
+ end
125
168
  end
126
169
 
127
170
  describe '.last' do
128
- it 'should return the query for the last record' do
129
- expect(query.last.to_s).to eq 'SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT 1'
171
+ context 'without any argument' do
172
+ it 'should return the query for the last record' do
173
+ expect(query.last.to_s).to eq 'SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT 1'
174
+ end
175
+
176
+ it "should not update the original query" do
177
+ new_query = query.last
178
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
179
+ expect(new_query.to_s).to eq 'SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT 1'
180
+ end
181
+ end
182
+
183
+ context 'with an argument' do
184
+ let(:last_argument) { 3 }
185
+
186
+ it 'should return the query for the last n records' do
187
+ expect(query.last(last_argument).to_s).to eq "SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT #{last_argument}"
188
+ end
189
+
190
+ it "should not update the original query" do
191
+ new_query = query.last last_argument
192
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
193
+ expect(new_query.to_s).to eq "SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT #{last_argument}"
194
+ end
130
195
  end
131
196
  end
132
197
 
@@ -138,6 +203,12 @@ describe ActiveForce::Query do
138
203
  it "should work with a condition" do
139
204
  expect(query.where("name = 'cool'").count.to_s).to eq "SELECT count(Id) FROM table_name WHERE (name = 'cool')"
140
205
  end
206
+
207
+ it "should not update the original query" do
208
+ query_with_count = query.where("name = 'cool'").count
209
+ expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
210
+ expect(query_with_count.to_s).to eq "SELECT count(Id) FROM table_name WHERE (name = 'cool')"
211
+ end
141
212
  end
142
213
 
143
214
  describe ".sum" do
@@ -225,6 +225,13 @@ module ActiveForce
225
225
  end
226
226
  end
227
227
 
228
+ context 'when assocation has a scope' do
229
+ it 'formulates the correct SOQL query with the scope applied' do
230
+ soql = Post.includes(:impossible_comments).where(id: '1234').to_s
231
+ expect(soql).to eq "SELECT Id, Title__c, BlogId, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comments__r WHERE (1 = 0)) FROM Post__c WHERE (Id = '1234')"
232
+ end
233
+ end
234
+
228
235
  context 'with namespaced SObjects' do
229
236
  it 'formulates the correct SOQL query' do
230
237
  soql = Salesforce::Quota.includes(:prez_clubs).where(id: '123').to_s
@@ -286,9 +293,26 @@ module ActiveForce
286
293
  end
287
294
  end
288
295
 
296
+ context 'has_one' do
297
+ context 'when assocation has a scope' do
298
+ it 'formulates the correct SOQL query with the scope applied' do
299
+ soql = Post.includes(:last_comment).where(id: '1234').to_s
300
+ expect(soql).to eq "SELECT Id, Title__c, BlogId, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__r WHERE (NOT ((Body__c = NULL))) ORDER BY CreatedDate DESC) FROM Post__c WHERE (Id = '1234')"
301
+ end
302
+ end
303
+
304
+ end
305
+
289
306
  context 'when invalid associations are passed' do
290
- it 'raises an error' do
291
- expect { Quota.includes(:invalid).find('123') }.to raise_error(ActiveForce::Association::InvalidAssociationError, 'Association named invalid was not found on Quota')
307
+ context 'when the association is not defined' do
308
+ it 'raises an error' do
309
+ expect { Quota.includes(:invalid).find('123') }.to raise_error(ActiveForce::Association::InvalidAssociationError, 'Association named invalid was not found on Quota')
310
+ end
311
+ end
312
+ context 'when the association is scoped and accepts an argument' do
313
+ it 'raises and error' do
314
+ expect { Post.includes(:reply_comments).find('1234')}.to raise_error(ActiveForce::Association::InvalidEagerLoadAssociation)
315
+ end
292
316
  end
293
317
  end
294
318
  end
@@ -313,20 +313,87 @@ describe ActiveForce::SObject do
313
313
  expect(Whizbang.create(text: 'some text')).to be_instance_of(Whizbang)
314
314
  end
315
315
  end
316
+
317
+ describe 'self.update' do
318
+ it 'uses the client to update the correct record' do
319
+ expect(client).to receive(:update!)
320
+ .with(Whizbang.table_name, { 'Id' => '12345678', 'Text_Label' => 'my text', 'Updated_From__c' => 'Rails' })
321
+ .and_return(true)
322
+ Whizbang.update('12345678', text: 'my text')
323
+ end
324
+ end
325
+
326
+ describe 'self.update!' do
327
+ it 'uses the client to update the correct record' do
328
+ expect(client).to receive(:update!)
329
+ .with(Whizbang.table_name, { 'Id' => '123456789', 'Text_Label' => 'some other text', 'Updated_From__c' => 'Rails' })
330
+ .and_return(true)
331
+ Whizbang.update('123456789', text: 'some other text')
332
+ end
333
+ end
316
334
  end
317
335
 
318
- describe "#count" do
319
- let(:count_response){ [Restforce::Mash.new(expr0: 1)] }
336
+ describe '.count' do
337
+ let(:response) { [Restforce::Mash.new(expr0: 1)] }
338
+
339
+ before do
340
+ allow(client).to receive(:query).and_return(response)
341
+ end
320
342
 
321
- it "responds to count" do
322
- expect(Whizbang).to respond_to(:count)
343
+ it 'sends the correct query to the client' do
344
+ expected = 'SELECT count(Id) FROM Whizbang__c'
345
+ Whizbang.count
346
+ expect(client).to have_received(:query).with(expected)
323
347
  end
324
348
 
325
- it "sends the query to the client" do
326
- expect(client).to receive(:query).and_return(count_response)
349
+ it 'returns the result from the response' do
327
350
  expect(Whizbang.count).to eq(1)
328
351
  end
329
352
 
353
+ it 'works with .where' do
354
+ expected = 'SELECT count(Id) FROM Whizbang__c WHERE (Boolean_Label = true)'
355
+ Whizbang.where(boolean: true).count
356
+ expect(client).to have_received(:query).with(expected)
357
+ end
358
+ end
359
+
360
+ describe '.sum' do
361
+ let(:response) { [Restforce::Mash.new(expr0: 22)] }
362
+
363
+ before do
364
+ allow(client).to receive(:query).and_return(response)
365
+ end
366
+
367
+ it 'raises ArgumentError if given blank' do
368
+ expect { Whizbang.sum(nil) }.to raise_error(ArgumentError, 'field is required')
369
+ end
370
+
371
+ it 'raises ArgumentError if given invalid field' do
372
+ expect { Whizbang.sum(:invalid) }
373
+ .to raise_error(ArgumentError, /field 'invalid' does not exist on Whizbang/i)
374
+ end
375
+
376
+ it 'sends the correct query to the client' do
377
+ expected = 'SELECT sum(Percent_Label) FROM Whizbang__c'
378
+ Whizbang.sum(:percent)
379
+ expect(client).to have_received(:query).with(expected)
380
+ end
381
+
382
+ it 'works when given a string field' do
383
+ expected = 'SELECT sum(Percent_Label) FROM Whizbang__c'
384
+ Whizbang.sum('percent')
385
+ expect(client).to have_received(:query).with(expected)
386
+ end
387
+
388
+ it 'returns the result from the response' do
389
+ expect(Whizbang.sum(:percent)).to eq(22)
390
+ end
391
+
392
+ it 'works with .where' do
393
+ expected = 'SELECT sum(Percent_Label) FROM Whizbang__c WHERE (Boolean_Label = true)'
394
+ Whizbang.where(boolean: true).sum(:percent)
395
+ expect(client).to have_received(:query).with(expected)
396
+ end
330
397
  end
331
398
 
332
399
  describe "#find_by" do
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_force
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eloy Espinaco
8
8
  - Pablo Oldani
9
9
  - Armando Andini
10
10
  - José Piccioni
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2023-07-31 00:00:00.000000000 Z
14
+ date: 2023-10-02 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activemodel
@@ -193,7 +193,7 @@ metadata:
193
193
  bug_tracker_uri: https://github.com/Beyond-Finance/active_force/issues
194
194
  changelog_uri: https://github.com/Beyond-Finance/active_force/blob/main/CHANGELOG.md
195
195
  source_code_uri: https://github.com/Beyond-Finance/active_force
196
- post_install_message:
196
+ post_install_message:
197
197
  rdoc_options: []
198
198
  require_paths:
199
199
  - lib
@@ -208,8 +208,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
208
  - !ruby/object:Gem::Version
209
209
  version: '0'
210
210
  requirements: []
211
- rubygems_version: 3.1.6
212
- signing_key:
211
+ rubygems_version: 3.3.26
212
+ signing_key:
213
213
  specification_version: 4
214
214
  summary: Help you implement models persisting on Sales Force within Rails using RESTForce
215
215
  test_files: