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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +107 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.mailmap +3 -0
- data/CHANGELOG.md +122 -41
- data/CODEOWNERS +2 -0
- data/Gemfile +0 -1
- data/README.md +116 -16
- data/active_force.gemspec +11 -4
- data/lib/active_force/active_query.rb +107 -6
- data/lib/active_force/association/association.rb +48 -4
- data/lib/active_force/association/belongs_to_association.rb +25 -11
- data/lib/active_force/association/eager_load_projection_builder.rb +9 -3
- data/lib/active_force/association/has_many_association.rb +19 -19
- data/lib/active_force/association/has_one_association.rb +30 -0
- data/lib/active_force/association/relation_model_builder.rb +1 -1
- data/lib/active_force/association.rb +6 -4
- data/lib/active_force/{attribute.rb → field.rb} +3 -3
- data/lib/active_force/mapping.rb +6 -32
- data/lib/active_force/query.rb +21 -2
- data/lib/active_force/sobject.rb +75 -29
- data/lib/active_force/version.rb +3 -1
- data/lib/active_force.rb +9 -0
- data/lib/active_model/type/salesforce/multipicklist.rb +29 -0
- data/lib/active_model/type/salesforce/percent.rb +22 -0
- data/lib/generators/active_force/model/model_generator.rb +32 -21
- data/lib/generators/active_force/model/templates/model.rb.erb +3 -1
- data/spec/active_force/active_query_spec.rb +200 -8
- data/spec/active_force/association/relation_model_builder_spec.rb +22 -0
- data/spec/active_force/association_spec.rb +253 -10
- data/spec/active_force/callbacks_spec.rb +1 -1
- data/spec/active_force/field_spec.rb +34 -0
- data/spec/active_force/query_spec.rb +26 -0
- data/spec/active_force/sobject/includes_spec.rb +11 -11
- data/spec/active_force/sobject_spec.rb +223 -16
- data/spec/fixtures/sobject/single_sobject_hash.yml +1 -1
- data/spec/spec_helper.rb +5 -2
- data/spec/support/bangwhiz.rb +7 -0
- data/spec/support/restforce_factories.rb +1 -1
- data/spec/support/sobjects.rb +17 -1
- data/spec/support/whizbang.rb +2 -2
- metadata +64 -25
- data/lib/active_attr/dirty.rb +0 -24
- data/spec/active_force/attribute_spec.rb +0 -27
data/lib/active_force/sobject.rb
CHANGED
@@ -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
|
17
|
-
include
|
18
|
-
|
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 :
|
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, :
|
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.
|
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
|
-
|
56
|
-
|
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.
|
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
|
-
|
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::
|
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
|
-
|
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::
|
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
|
-
|
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::
|
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
|
-
|
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
|
-
|
173
|
+
clear_changes_information
|
148
174
|
self
|
149
175
|
end
|
150
176
|
|
151
|
-
def write_value
|
152
|
-
if association = self.class.find_association(
|
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
|
-
|
157
|
-
|
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
|
221
|
+
errors.add(:base, exception.message)
|
185
222
|
false
|
186
223
|
end
|
187
224
|
|
188
225
|
def attributes_for_sfdb
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
248
|
+
ActiveForce.sfdc_client
|
203
249
|
end
|
204
250
|
|
205
251
|
def sfdc_client
|
data/lib/active_force/version.rb
CHANGED
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
|
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
|
10
|
-
template "model.rb.erb", "app/models/#{@class_name.
|
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
|
-
|
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
|
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::
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
@@ -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){
|
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
|
31
|
-
|
32
|
-
|
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 = ''
|
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 = ''
|
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
|