active_force 0.7.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|