active_force 0.16.0 → 0.18.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.
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: