active_force 0.7.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +107 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.mailmap +3 -0
  6. data/CHANGELOG.md +122 -41
  7. data/CODEOWNERS +2 -0
  8. data/Gemfile +0 -1
  9. data/README.md +116 -16
  10. data/active_force.gemspec +11 -4
  11. data/lib/active_force/active_query.rb +107 -6
  12. data/lib/active_force/association/association.rb +48 -4
  13. data/lib/active_force/association/belongs_to_association.rb +25 -11
  14. data/lib/active_force/association/eager_load_projection_builder.rb +9 -3
  15. data/lib/active_force/association/has_many_association.rb +19 -19
  16. data/lib/active_force/association/has_one_association.rb +30 -0
  17. data/lib/active_force/association/relation_model_builder.rb +1 -1
  18. data/lib/active_force/association.rb +6 -4
  19. data/lib/active_force/{attribute.rb → field.rb} +3 -3
  20. data/lib/active_force/mapping.rb +6 -32
  21. data/lib/active_force/query.rb +21 -2
  22. data/lib/active_force/sobject.rb +75 -29
  23. data/lib/active_force/version.rb +3 -1
  24. data/lib/active_force.rb +9 -0
  25. data/lib/active_model/type/salesforce/multipicklist.rb +29 -0
  26. data/lib/active_model/type/salesforce/percent.rb +22 -0
  27. data/lib/generators/active_force/model/model_generator.rb +32 -21
  28. data/lib/generators/active_force/model/templates/model.rb.erb +3 -1
  29. data/spec/active_force/active_query_spec.rb +200 -8
  30. data/spec/active_force/association/relation_model_builder_spec.rb +22 -0
  31. data/spec/active_force/association_spec.rb +253 -10
  32. data/spec/active_force/callbacks_spec.rb +1 -1
  33. data/spec/active_force/field_spec.rb +34 -0
  34. data/spec/active_force/query_spec.rb +26 -0
  35. data/spec/active_force/sobject/includes_spec.rb +11 -11
  36. data/spec/active_force/sobject_spec.rb +223 -16
  37. data/spec/fixtures/sobject/single_sobject_hash.yml +1 -1
  38. data/spec/spec_helper.rb +5 -2
  39. data/spec/support/bangwhiz.rb +7 -0
  40. data/spec/support/restforce_factories.rb +1 -1
  41. data/spec/support/sobjects.rb +17 -1
  42. data/spec/support/whizbang.rb +2 -2
  43. metadata +64 -25
  44. data/lib/active_attr/dirty.rb +0 -24
  45. data/spec/active_force/attribute_spec.rb +0 -27
@@ -1,6 +1,4 @@
1
1
  require 'active_model'
2
- require 'active_attr'
3
- require 'active_attr/dirty'
4
2
  require 'active_force/active_query'
5
3
  require 'active_force/association'
6
4
  require 'active_force/mapping'
@@ -13,18 +11,26 @@ module ActiveForce
13
11
  class RecordInvalid < StandardError;end
14
12
 
15
13
  class SObject
16
- include ActiveAttr::Model
17
- include ActiveAttr::Dirty
18
- extend ActiveForce::Association
14
+ include ActiveModel::API
15
+ include ActiveModel::AttributeMethods
16
+ include ActiveModel::Attributes
17
+ include ActiveModel::Model
18
+ include ActiveModel::Dirty
19
19
  extend ActiveModel::Callbacks
20
+ include ActiveModel::Serializers::JSON
21
+ extend ActiveForce::Association
22
+
20
23
 
21
- define_model_callbacks :save, :create, :update
24
+ define_model_callbacks :build, :create, :update, :save, :destroy
22
25
 
23
26
  class_attribute :mappings, :table_name
24
27
 
28
+ attr_accessor :id, :title
29
+
25
30
  class << self
26
31
  extend Forwardable
27
- def_delegators :query, :where, :first, :last, :all, :find, :find_by, :count, :includes, :limit, :order, :select
32
+ def_delegators :query, :not, :or, :where, :first, :last, :all, :find, :find!, :find_by, :find_by!, :sum, :count,
33
+ :includes, :limit, :order, :select, :none, :pluck
28
34
  def_delegators :mapping, :table, :table_name, :custom_table?, :mappings
29
35
 
30
36
  private
@@ -49,13 +55,24 @@ module ActiveForce
49
55
  ActiveForce::ActiveQuery.new self
50
56
  end
51
57
 
52
- def self.build mash
58
+ def self.describe
59
+ sfdc_client.describe(table_name)
60
+ end
61
+
62
+ attr_accessor :build_attributes
63
+ def self.build mash, association_mapping={}
53
64
  return unless mash
54
65
  sobject = new
55
- mash.each do |column, sf_value|
56
- sobject.write_value column, sf_value
66
+ sobject.build_attributes = mash[:build_attributes] || mash
67
+ sobject.run_callbacks(:build) do
68
+ mash.each do |column, value|
69
+ if association_mapping.has_key?(column.downcase)
70
+ column = association_mapping[column.downcase]
71
+ end
72
+ sobject.write_value column, value
73
+ end
57
74
  end
58
- sobject.changed_attributes.clear
75
+ sobject.clear_changes_information
59
76
  sobject
60
77
  end
61
78
 
@@ -65,7 +82,7 @@ module ActiveForce
65
82
  run_callbacks :save do
66
83
  run_callbacks :update do
67
84
  sfdc_client.update! table_name, attributes_for_sfdb
68
- changed_attributes.clear
85
+ clear_changes_information
69
86
  end
70
87
  end
71
88
  true
@@ -75,7 +92,7 @@ module ActiveForce
75
92
 
76
93
  def update_attributes attributes = {}
77
94
  update_attributes! attributes
78
- rescue Faraday::Error::ClientError, RecordInvalid => error
95
+ rescue Faraday::ClientError, RecordInvalid => error
79
96
  handle_save_error error
80
97
  end
81
98
 
@@ -86,7 +103,7 @@ module ActiveForce
86
103
  run_callbacks :save do
87
104
  run_callbacks :create do
88
105
  self.id = sfdc_client.create! table_name, attributes_for_sfdb
89
- changed_attributes.clear
106
+ clear_changes_information
90
107
  end
91
108
  end
92
109
  self
@@ -94,13 +111,15 @@ module ActiveForce
94
111
 
95
112
  def create
96
113
  create!
97
- rescue Faraday::Error::ClientError, RecordInvalid => error
114
+ rescue Faraday::ClientError, RecordInvalid => error
98
115
  handle_save_error error
99
116
  self
100
117
  end
101
118
 
102
119
  def destroy
103
- sfdc_client.destroy! self.class.table_name, id
120
+ run_callbacks(:destroy) do
121
+ sfdc_client.destroy! self.class.table_name, id
122
+ end
104
123
  end
105
124
 
106
125
  def self.create args
@@ -123,7 +142,7 @@ module ActiveForce
123
142
 
124
143
  def save
125
144
  save!
126
- rescue Faraday::Error::ClientError, RecordInvalid => error
145
+ rescue Faraday::ClientError, RecordInvalid => error
127
146
  handle_save_error error
128
147
  end
129
148
 
@@ -136,27 +155,45 @@ module ActiveForce
136
155
  end
137
156
 
138
157
  def self.field field_name, args = {}
158
+ options = args.except(:as, :from, :sfdc_name)
139
159
  mapping.field field_name, args
140
- attribute field_name
160
+ cast_type = args.fetch(:as, :string)
161
+ attribute field_name, cast_type, **options
162
+ define_attribute_methods field_name
163
+ end
164
+
165
+ def modified_attributes
166
+ attributes.select{ |attr, key| changed.include? attr.to_s }
141
167
  end
142
168
 
143
169
  def reload
144
170
  association_cache.clear
145
171
  reloaded = self.class.find(id)
146
172
  self.attributes = reloaded.attributes
147
- changed_attributes.clear
173
+ clear_changes_information
148
174
  self
149
175
  end
150
176
 
151
- def write_value column, value
152
- if association = self.class.find_association(column)
177
+ def write_value key, value
178
+ if association = self.class.find_association(key.to_sym)
153
179
  field = association.relation_name
154
180
  value = Association::RelationModelBuilder.build(association, value)
181
+ elsif key.to_sym.in?(mappings.keys)
182
+ # key is a field name
183
+ field = key
155
184
  else
156
- field = mappings.invert[column]
157
- value = self.class.mapping.translate_value value, field unless value.nil?
185
+ # Assume key is an SFDC column
186
+ field = mappings.key(key)
158
187
  end
159
- send "#{field}=", value if field
188
+ send "#{field}=", value if field && respond_to?(field)
189
+ end
190
+
191
+ def [](name)
192
+ send(name.to_sym)
193
+ end
194
+
195
+ def []=(name,value)
196
+ send("#{name.to_sym}=", value)
160
197
  end
161
198
 
162
199
  private
@@ -181,14 +218,23 @@ module ActiveForce
181
218
  def logger_output action, exception, params = {}
182
219
  logger = Logger.new(STDOUT)
183
220
  logger.info("[SFDC] [#{self.class.model_name}] [#{self.class.table_name}] Error while #{ action }, params: #{params}, error: #{exception.inspect}")
184
- errors[:base] << exception.message
221
+ errors.add(:base, exception.message)
185
222
  false
186
223
  end
187
224
 
188
225
  def attributes_for_sfdb
189
- attrs = self.class.mapping.translate_to_sf(attributes_and_changes)
190
- attrs.merge!({'Id' => id }) if persisted?
191
- attrs
226
+ attrs_to_change = persisted? ? attributes_for_update : attributes_for_create
227
+ self.class.mapping.translate_to_sf(@attributes.values_for_database.slice(*attrs_to_change))
228
+ end
229
+
230
+ def attributes_for_create
231
+ @attributes.each_value.select { |value| value.is_a?(ActiveModel::Attribute::UserProvidedDefault) }
232
+ .map(&:name)
233
+ .concat(changed)
234
+ end
235
+
236
+ def attributes_for_update
237
+ ['id'].concat(changed)
192
238
  end
193
239
 
194
240
  def self.picklist field
@@ -199,7 +245,7 @@ module ActiveForce
199
245
  end
200
246
 
201
247
  def self.sfdc_client
202
- @client ||= Restforce.new
248
+ ActiveForce.sfdc_client
203
249
  end
204
250
 
205
251
  def sfdc_client
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveForce
2
- VERSION = "0.7.0"
4
+ VERSION = '0.15.0'
3
5
  end
data/lib/active_force.rb CHANGED
@@ -1,6 +1,15 @@
1
+ require 'active_model/type/salesforce/multipicklist'
2
+ require 'active_model/type/salesforce/percent'
1
3
  require 'active_force/version'
2
4
  require 'active_force/sobject'
3
5
  require 'active_force/query'
4
6
 
5
7
  module ActiveForce
8
+
9
+ class << self
10
+ attr_accessor :sfdc_client
11
+ end
12
+
13
+ self.sfdc_client = Restforce.new
14
+
6
15
  end
@@ -0,0 +1,29 @@
1
+ require 'active_model'
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module Salesforce
6
+ class Multipicklist < ActiveModel::Type::Value
7
+ include ActiveModel::Type::Helpers::Mutable
8
+
9
+ def type
10
+ :multipicklist
11
+ end
12
+
13
+ def deserialize(value)
14
+ value.to_s.split(';')
15
+ end
16
+
17
+ def serialize(value)
18
+ return if value.blank?
19
+
20
+ return value if value.is_a?(::String)
21
+
22
+ value.to_a.reject(&:empty?).join(';')
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ ActiveModel::Type.register(:multipicklist, ActiveModel::Type::Salesforce::Multipicklist)
@@ -0,0 +1,22 @@
1
+ require 'active_model'
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module Salesforce
6
+ class Percent < ActiveModel::Type::Value
7
+
8
+ def type
9
+ :percent
10
+ end
11
+
12
+ private
13
+
14
+ def cast_value(value)
15
+ value.to_f
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveModel::Type.register(:percent, ActiveModel::Type::Salesforce::Percent)
@@ -1,35 +1,48 @@
1
1
  module ActiveForce
2
2
  class ModelGenerator < Rails::Generators::NamedBase
3
- desc 'This generator loads the table fields from SFDC and generates the fields for the SObject with a more ruby names'
3
+ desc 'This generator loads the table fields from SFDC and generates the fields for the SObject with a more Ruby name'
4
4
 
5
5
  source_root File.expand_path('../templates', __FILE__)
6
+ argument :namespace, type: :string, optional: true, default: ''
7
+
8
+ SALESFORCE_TO_ACTIVEMODEL_TYPE_MAP = {
9
+ 'boolean' => :boolean,
10
+ 'double' => :float,
11
+ 'percentage' => :float,
12
+ 'currency' => :float,
13
+ 'date' => :date,
14
+ 'datetime' => :datetime,
15
+ 'int' => :integer,
16
+ }
6
17
 
7
18
  def create_model_file
8
19
  @table_name = file_name.capitalize
9
- @class_name = @table_name.gsub '__c', ''
10
- template "model.rb.erb", "app/models/#{@class_name.downcase}.rb" if table_exists?
20
+ @class_name = prepare_namespace + @table_name.gsub('__c', '')
21
+ template "model.rb.erb", "app/models/#{@class_name.underscore}.rb" if table_exists?
11
22
  end
12
23
 
13
24
  protected
14
25
 
15
- Attribute = Struct.new :field, :column
26
+ def prepare_namespace
27
+ @namespace.present? ? @namespace + '::' : @namespace
28
+ end
29
+
30
+ Attribute = Struct.new :field, :column, :type
16
31
 
17
- def attributes
18
- @attributes ||= sfdc_columns.map do |column|
19
- Attribute.new column_to_field(column), column
32
+ def attributes
33
+ @attributes ||= sfdc_columns.sort_by { |col| col[:name].downcase }.map do |column|
34
+ Attribute.new column_to_field(column.name), column.name, saleforce_to_active_model_type(column.type)
20
35
  end
21
36
  @attributes - [:id]
22
37
  end
23
38
 
24
39
  def sfdc_columns
25
- @columns ||= ActiveForce::SObject.sfdc_client.describe(@table_name).fields.map do |field|
26
- field.name
27
- end
40
+ @columns ||= ActiveForce::SObject.sfdc_client.describe(@table_name).fields
28
41
  end
29
42
 
30
43
  def table_exists?
31
44
  !! sfdc_columns
32
- rescue Faraday::Error::ResourceNotFound
45
+ rescue Faraday::ResourceNotFound
33
46
  puts "The specified table name is not found. Be sure to append __c if it's custom"
34
47
  end
35
48
 
@@ -38,7 +51,7 @@ module ActiveForce
38
51
  end
39
52
 
40
53
  def attribute_line attribute
41
- "field :#{ attribute.field },#{ space_justify attribute.field } from: '#{ attribute.column }'"
54
+ "field :#{ attribute.field },#{ space_justify attribute.field } from: '#{ attribute.column }'#{ add_type(attribute.type) }"
42
55
  end
43
56
 
44
57
  def space_justify field_name
@@ -47,16 +60,14 @@ module ActiveForce
47
60
  " " * justify_count
48
61
  end
49
62
 
50
-
51
- class String
52
- def underscore
53
- self.gsub(/::/, '/').
54
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
55
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
56
- tr("-", "_").
57
- downcase
58
- end
63
+ def add_type(type)
64
+ # String is the default so no need to add it
65
+ return '' if type == :string
66
+ ", as: :#{ type }"
59
67
  end
60
68
 
69
+ def saleforce_to_active_model_type type
70
+ SALESFORCE_TO_ACTIVEMODEL_TYPE_MAP.fetch(type, :string)
71
+ end
61
72
  end
62
73
  end
@@ -1,4 +1,6 @@
1
- class <%= @class_name %> < ActiveForce::SObject
1
+ class <%= @class_name.classify %> < ActiveForce::SObject
2
+ self.table_name = '<%= @table_name %>'
3
+
2
4
  <% attributes.each do |attribute| -%>
3
5
  <%= attribute_line attribute %>
4
6
  <% end -%>
@@ -8,9 +8,16 @@ describe ActiveForce::ActiveQuery do
8
8
  mappings: mappings
9
9
  })
10
10
  end
11
- let(:mappings){ { field: "Field__c", other_field: "Other_Field" } }
11
+ let(:mappings){ { id: "Id", field: "Field__c", other_field: "Other_Field" } }
12
12
  let(:client){ double("client") }
13
- let(:active_query){ ActiveForce::ActiveQuery.new(sobject) }
13
+ let(:active_query){ described_class.new(sobject) }
14
+ let(:api_result) do
15
+ [
16
+ {"Id" => "0000000000AAAAABBB"},
17
+ {"Id" => "0000000000CCCCCDDD"}
18
+ ]
19
+ end
20
+
14
21
 
15
22
  before do
16
23
  allow(active_query).to receive(:sfdc_client).and_return client
@@ -19,7 +26,7 @@ describe ActiveForce::ActiveQuery do
19
26
 
20
27
  describe "to_a" do
21
28
  before do
22
- expect(client).to receive(:query)
29
+ expect(client).to receive(:query).and_return(api_result)
23
30
  end
24
31
 
25
32
  it "should return an array of objects" do
@@ -27,9 +34,9 @@ describe ActiveForce::ActiveQuery do
27
34
  expect(result).to be_a Array
28
35
  end
29
36
 
30
- it "should allow to chain query methods" do
31
- result = active_query.where("Text_Label = 'foo'").where("Checkbox_Label = true").to_a
32
- expect(result).to be_a Array
37
+ it "should decorate the array of objects" do
38
+ expect(sobject).to receive(:decorate)
39
+ active_query.where("Text_Label = 'foo'").to_a
33
40
  end
34
41
  end
35
42
 
@@ -56,6 +63,24 @@ describe ActiveForce::ActiveQuery do
56
63
  expect(active_query.to_s).to end_with("(Field__c = 'hello')")
57
64
  end
58
65
 
66
+ it "formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if it's a DateTime" do
67
+ value = DateTime.now
68
+ active_query.where(field: value)
69
+ expect(active_query.to_s).to end_with("(Field__c = #{value.iso8601})")
70
+ end
71
+
72
+ it "formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if it's a Time" do
73
+ value = Time.now
74
+ active_query.where(field: value)
75
+ expect(active_query.to_s).to end_with("(Field__c = #{value.iso8601})")
76
+ end
77
+
78
+ it "formats as YYYY-MM-DD and does not enclose in quotes if it's a Date" do
79
+ value = Date.today
80
+ active_query.where(field: value)
81
+ expect(active_query.to_s).to end_with("(Field__c = #{value.iso8601})")
82
+ end
83
+
59
84
  it "puts NULL when a field is set as nil" do
60
85
  active_query.where field: nil
61
86
  expect(active_query.to_s).to end_with("(Field__c = NULL)")
@@ -84,6 +109,24 @@ describe ActiveForce::ActiveQuery do
84
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')")
85
110
  end
86
111
 
112
+ it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a DateTime' do
113
+ value = DateTime.now
114
+ active_query.where('Field__c > ?', value)
115
+ expect(active_query.to_s).to end_with("(Field__c > #{value.iso8601})")
116
+ end
117
+
118
+ it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a Time' do
119
+ value = Time.now
120
+ active_query.where('Field__c > ?', value)
121
+ expect(active_query.to_s).to end_with("(Field__c > #{value.iso8601})")
122
+ end
123
+
124
+ it 'formats as YYYY-MM-DD and does not enclose in quotes if value is a Date' do
125
+ value = Date.today
126
+ active_query.where('Field__c > ?', value)
127
+ expect(active_query.to_s).to end_with("(Field__c > #{value.iso8601})")
128
+ end
129
+
87
130
  it 'complains when there given an incorrect number of bind parameters' do
88
131
  expect{
89
132
  active_query.where('Field__c = ? AND Other_Field__c = ? AND Name = ?', 123, 321)
@@ -101,6 +144,24 @@ describe ActiveForce::ActiveQuery do
101
144
  expect(active_query.to_s).to eq("SELECT Id FROM table_name WHERE (Field__c = NULL)")
102
145
  end
103
146
 
147
+ it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a DateTime' do
148
+ 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})")
151
+ end
152
+
153
+ it 'formats as YYYY-MM-DDThh:mm:ss-hh:mm and does not enclose in quotes if value is a Time' do
154
+ 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})")
157
+ end
158
+
159
+ it 'formats as YYYY-MM-DD and does not enclose in quotes if value is a Date' do
160
+ 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})")
163
+ end
164
+
104
165
  it 'accepts multiple bind parameters' do
105
166
  active_query.where('Field__c = :field AND Other_Field__c = :other_field AND Name = :name', field: 123, other_field: 321, name: 'Bob')
106
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')")
@@ -120,6 +181,41 @@ describe ActiveForce::ActiveQuery do
120
181
  end
121
182
  end
122
183
 
184
+ describe '#where' do
185
+ before do
186
+ allow(client).to receive(:query).with("SELECT Id FROM table_name WHERE (Text_Label = 'foo')").and_return(api_result1)
187
+ allow(client).to receive(:query).with("SELECT Id FROM table_name WHERE (Text_Label = 'foo') AND (Checkbox_Label = true)").and_return(api_result2)
188
+ end
189
+ let(:api_result1) do
190
+ [
191
+ {"Id" => "0000000000AAAAABBB"},
192
+ {"Id" => "0000000000CCCCCDDD"},
193
+ {"Id" => "0000000000EEEEEFFF"}
194
+ ]
195
+ end
196
+ let(:api_result2) do
197
+ [
198
+ {"Id" => "0000000000EEEEEFFF"}
199
+ ]
200
+ end
201
+ it 'allows method chaining' do
202
+ result = active_query.where("Text_Label = 'foo'").where("Checkbox_Label = true")
203
+ expect(result).to be_a described_class
204
+ end
205
+
206
+ 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
214
+ end
215
+ end
216
+
217
+ end
218
+
123
219
  describe "#find_by" do
124
220
  it "should query the client, with the SFDC field names and correctly enclosed values" do
125
221
  expect(client).to receive :query
@@ -128,6 +224,50 @@ describe ActiveForce::ActiveQuery do
128
224
  end
129
225
  end
130
226
 
227
+ describe '#find_by!' do
228
+ it 'raises if record not found' do
229
+ allow(client).to receive(:query).and_return(build_restforce_collection)
230
+ expect { active_query.find_by!(field: 123) }
231
+ .to raise_error(ActiveForce::RecordNotFound, "Couldn't find #{sobject.table_name} with {:field=>123}")
232
+ end
233
+ end
234
+
235
+ describe '#find!' do
236
+ let(:id) { 'test_id' }
237
+
238
+ before do
239
+ allow(client).to receive(:query).and_return(build_restforce_collection([{ 'Id' => id }]))
240
+ end
241
+
242
+ it 'queries for single record by given id' do
243
+ active_query.find!(id)
244
+ expect(client).to have_received(:query).with("SELECT Id FROM #{sobject.table_name} WHERE (Id = '#{id}') LIMIT 1")
245
+ end
246
+
247
+ context 'when record is found' do
248
+ let(:record) { build_restforce_sobject(id: id) }
249
+
250
+ before do
251
+ allow(active_query).to receive(:build).and_return(record)
252
+ end
253
+
254
+ it 'returns the record' do
255
+ expect(active_query.find!(id)).to eq(record)
256
+ end
257
+ end
258
+
259
+ context 'when no record is found' do
260
+ before do
261
+ allow(client).to receive(:query).and_return(build_restforce_collection)
262
+ end
263
+
264
+ it 'raises RecordNotFound' do
265
+ expect { active_query.find!(id) }
266
+ .to raise_error(ActiveForce::RecordNotFound, "Couldn't find #{sobject.table_name} with id #{id}")
267
+ end
268
+ end
269
+ end
270
+
131
271
  describe "responding as an enumerable" do
132
272
  before do
133
273
  expect(active_query).to receive(:to_a).and_return([])
@@ -147,7 +287,7 @@ describe ActiveForce::ActiveQuery do
147
287
  let(:quote_input){ "' OR Id!=NULL OR Id='" }
148
288
  let(:backslash_input){ "\\" }
149
289
  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=''')" }
290
+ let(:expected_query){ "SELECT Id FROM table_name WHERE (Backslash_Field__c = '\\\\' AND NumberField = 123 AND QuoteField = '\\' OR Id!=NULL OR Id=\\'')" }
151
291
 
152
292
  it 'escapes quotes and backslashes in bind parameters' do
153
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)
@@ -161,7 +301,59 @@ describe ActiveForce::ActiveQuery do
161
301
 
162
302
  it 'escapes quotes and backslashes in hash conditions' do
163
303
  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=''')")
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=\\'')")
305
+ end
306
+ end
307
+
308
+ describe '#none' do
309
+ it 'returns a query with a where clause that is impossible to satisfy' do
310
+ expect(active_query.none.to_s).to eq "SELECT Id FROM table_name WHERE (Id = '111111111111111111') AND (Id = '000000000000000000')"
311
+ end
312
+
313
+ it 'does not query the API' do
314
+ expect(client).to_not receive :query
315
+ active_query.none.to_a
316
+ end
317
+ end
318
+
319
+ describe '#loaded?' do
320
+ subject { active_query.loaded? }
321
+
322
+ before do
323
+ active_query.instance_variable_set(:@records, records)
324
+ end
325
+
326
+ context 'when there are records loaded in memory' do
327
+ let(:records) { nil }
328
+
329
+ it { is_expected.to be_falsey }
330
+ end
331
+
332
+ context 'when there are records loaded in memory' do
333
+ let(:records) { [build_restforce_sobject(id: 1)] }
334
+
335
+ it { is_expected.to be_truthy }
336
+ end
337
+ end
338
+
339
+ describe "#order" do
340
+ context 'when it is symbol' do
341
+ it "should add an order condition with actual SF field name" do
342
+ expect(active_query.order(:field).to_s).to eq "SELECT Id FROM table_name ORDER BY Field__c"
343
+ end
344
+ end
345
+
346
+ context 'when it is string - raw soql' do
347
+ it "should add an order condition same as the string provided" do
348
+ expect(active_query.order('Field__c').to_s).to eq "SELECT Id FROM table_name ORDER BY Field__c"
349
+ end
350
+ end
351
+
352
+ context 'when it is multiple columns' do
353
+ it "should add an order condition with actual SF field name and the provided order type" do
354
+ expect(active_query.order(:other_field, field: :desc).to_s).to eq "SELECT Id FROM table_name ORDER BY Other_Field, Field__c DESC"
165
355
  end
356
+ end
357
+
166
358
  end
167
359
  end