active_force 0.7.1 → 0.15.1

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