active_force 0.7.1 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) 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 +120 -42
  7. data/CODEOWNERS +2 -0
  8. data/Gemfile +0 -1
  9. data/README.md +100 -21
  10. data/active_force.gemspec +11 -4
  11. data/lib/active_force/active_query.rb +92 -6
  12. data/lib/active_force/association/association.rb +47 -3
  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 +73 -28
  23. data/lib/active_force/version.rb +3 -1
  24. data/lib/active_force.rb +2 -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 +252 -9
  32. data/spec/active_force/field_spec.rb +34 -0
  33. data/spec/active_force/query_spec.rb +26 -0
  34. data/spec/active_force/sobject/includes_spec.rb +10 -10
  35. data/spec/active_force/sobject_spec.rb +156 -14
  36. data/spec/fixtures/sobject/single_sobject_hash.yml +1 -1
  37. data/spec/spec_helper.rb +5 -2
  38. data/spec/support/bangwhiz.rb +7 -0
  39. data/spec/support/restforce_factories.rb +1 -1
  40. data/spec/support/sobjects.rb +17 -1
  41. data/spec/support/whizbang.rb +2 -2
  42. metadata +64 -26
  43. data/lib/active_attr/dirty.rb +0 -24
  44. 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,25 @@ 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, :includes, :limit, :order, :select, :none
28
33
  def_delegators :mapping, :table, :table_name, :custom_table?, :mappings
29
34
 
30
35
  private
@@ -49,13 +54,24 @@ module ActiveForce
49
54
  ActiveForce::ActiveQuery.new self
50
55
  end
51
56
 
52
- def self.build mash
57
+ def self.describe
58
+ sfdc_client.describe(table_name)
59
+ end
60
+
61
+ attr_accessor :build_attributes
62
+ def self.build mash, association_mapping={}
53
63
  return unless mash
54
64
  sobject = new
55
- mash.each do |column, sf_value|
56
- sobject.write_value column, sf_value
65
+ sobject.build_attributes = mash[:build_attributes] || mash
66
+ sobject.run_callbacks(:build) do
67
+ mash.each do |column, value|
68
+ if association_mapping.has_key?(column.downcase)
69
+ column = association_mapping[column.downcase]
70
+ end
71
+ sobject.write_value column, value
72
+ end
57
73
  end
58
- sobject.changed_attributes.clear
74
+ sobject.clear_changes_information
59
75
  sobject
60
76
  end
61
77
 
@@ -65,7 +81,7 @@ module ActiveForce
65
81
  run_callbacks :save do
66
82
  run_callbacks :update do
67
83
  sfdc_client.update! table_name, attributes_for_sfdb
68
- changed_attributes.clear
84
+ clear_changes_information
69
85
  end
70
86
  end
71
87
  true
@@ -75,7 +91,7 @@ module ActiveForce
75
91
 
76
92
  def update_attributes attributes = {}
77
93
  update_attributes! attributes
78
- rescue Faraday::Error::ClientError, RecordInvalid => error
94
+ rescue Faraday::ClientError, RecordInvalid => error
79
95
  handle_save_error error
80
96
  end
81
97
 
@@ -86,7 +102,7 @@ module ActiveForce
86
102
  run_callbacks :save do
87
103
  run_callbacks :create do
88
104
  self.id = sfdc_client.create! table_name, attributes_for_sfdb
89
- changed_attributes.clear
105
+ clear_changes_information
90
106
  end
91
107
  end
92
108
  self
@@ -94,13 +110,15 @@ module ActiveForce
94
110
 
95
111
  def create
96
112
  create!
97
- rescue Faraday::Error::ClientError, RecordInvalid => error
113
+ rescue Faraday::ClientError, RecordInvalid => error
98
114
  handle_save_error error
99
115
  self
100
116
  end
101
117
 
102
118
  def destroy
103
- sfdc_client.destroy! self.class.table_name, id
119
+ run_callbacks(:destroy) do
120
+ sfdc_client.destroy! self.class.table_name, id
121
+ end
104
122
  end
105
123
 
106
124
  def self.create args
@@ -123,7 +141,7 @@ module ActiveForce
123
141
 
124
142
  def save
125
143
  save!
126
- rescue Faraday::Error::ClientError, RecordInvalid => error
144
+ rescue Faraday::ClientError, RecordInvalid => error
127
145
  handle_save_error error
128
146
  end
129
147
 
@@ -136,27 +154,45 @@ module ActiveForce
136
154
  end
137
155
 
138
156
  def self.field field_name, args = {}
157
+ options = args.except(:as, :from, :sfdc_name)
139
158
  mapping.field field_name, args
140
- attribute field_name
159
+ cast_type = args.fetch(:as, :string)
160
+ attribute field_name, cast_type, **options
161
+ define_attribute_methods field_name
162
+ end
163
+
164
+ def modified_attributes
165
+ attributes.select{ |attr, key| changed.include? attr.to_s }
141
166
  end
142
167
 
143
168
  def reload
144
169
  association_cache.clear
145
170
  reloaded = self.class.find(id)
146
171
  self.attributes = reloaded.attributes
147
- changed_attributes.clear
172
+ clear_changes_information
148
173
  self
149
174
  end
150
175
 
151
- def write_value column, value
152
- if association = self.class.find_association(column)
176
+ def write_value key, value
177
+ if association = self.class.find_association(key.to_sym)
153
178
  field = association.relation_name
154
179
  value = Association::RelationModelBuilder.build(association, value)
180
+ elsif key.to_sym.in?(mappings.keys)
181
+ # key is a field name
182
+ field = key
155
183
  else
156
- field = mappings.invert[column]
157
- value = self.class.mapping.translate_value value, field unless value.nil?
184
+ # Assume key is an SFDC column
185
+ field = mappings.key(key)
158
186
  end
159
- send "#{field}=", value if field
187
+ send "#{field}=", value if field && respond_to?(field)
188
+ end
189
+
190
+ def [](name)
191
+ send(name.to_sym)
192
+ end
193
+
194
+ def []=(name,value)
195
+ send("#{name.to_sym}=", value)
160
196
  end
161
197
 
162
198
  private
@@ -181,14 +217,23 @@ module ActiveForce
181
217
  def logger_output action, exception, params = {}
182
218
  logger = Logger.new(STDOUT)
183
219
  logger.info("[SFDC] [#{self.class.model_name}] [#{self.class.table_name}] Error while #{ action }, params: #{params}, error: #{exception.inspect}")
184
- errors[:base] << exception.message
220
+ errors.add(:base, exception.message)
185
221
  false
186
222
  end
187
223
 
188
224
  def attributes_for_sfdb
189
- attrs = self.class.mapping.translate_to_sf(attributes_and_changes)
190
- attrs.merge!({'Id' => id }) if persisted?
191
- attrs
225
+ attrs_to_change = persisted? ? attributes_for_update : attributes_for_create
226
+ self.class.mapping.translate_to_sf(@attributes.values_for_database.slice(*attrs_to_change))
227
+ end
228
+
229
+ def attributes_for_create
230
+ @attributes.each_value.select { |value| value.is_a?(ActiveModel::Attribute::UserProvidedDefault) }
231
+ .map(&:name)
232
+ .concat(changed)
233
+ end
234
+
235
+ def attributes_for_update
236
+ ['id'].concat(changed)
192
237
  end
193
238
 
194
239
  def self.picklist field
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveForce
2
- VERSION = "0.7.1"
4
+ VERSION = '0.15.1'
3
5
  end
data/lib/active_force.rb CHANGED
@@ -1,3 +1,5 @@
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'
@@ -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
@@ -56,6 +56,28 @@ module ActiveForce
56
56
  end
57
57
  end
58
58
  end
59
+
60
+ context 'has_one' do
61
+ let(:association){ HasOneAssociation.new(HasOneParent, :has_one_child) }
62
+
63
+ context 'with a value' do
64
+ let(:value) do
65
+ build_restforce_sobject 'Id' => '213'
66
+ end
67
+
68
+ it 'returns a child' do
69
+ expect(instance.build_relation_model).to be_a HasOneChild
70
+ end
71
+ end
72
+
73
+ context 'without a value' do
74
+ let(:value){ nil }
75
+
76
+ it 'returns nil' do
77
+ expect(instance.build_relation_model).to be_nil
78
+ end
79
+ end
80
+ end
59
81
  end
60
82
  end
61
83
  end