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.
- 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 +120 -42
- data/CODEOWNERS +2 -0
- data/Gemfile +0 -1
- data/README.md +100 -21
- data/active_force.gemspec +11 -4
- data/lib/active_force/active_query.rb +92 -6
- data/lib/active_force/association/association.rb +47 -3
- 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 +73 -28
- data/lib/active_force/version.rb +3 -1
- data/lib/active_force.rb +2 -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 +252 -9
- 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 +10 -10
- data/spec/active_force/sobject_spec.rb +156 -14
- 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 -26
- 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,25 @@ 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, :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.
|
|
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
|
-
|
|
56
|
-
|
|
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.
|
|
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
|
-
|
|
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::
|
|
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
|
-
|
|
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::
|
|
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
|
-
|
|
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::
|
|
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
|
-
|
|
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
|
-
|
|
172
|
+
clear_changes_information
|
|
148
173
|
self
|
|
149
174
|
end
|
|
150
175
|
|
|
151
|
-
def write_value
|
|
152
|
-
if association = self.class.find_association(
|
|
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
|
-
|
|
157
|
-
|
|
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
|
|
220
|
+
errors.add(:base, exception.message)
|
|
185
221
|
false
|
|
186
222
|
end
|
|
187
223
|
|
|
188
224
|
def attributes_for_sfdb
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
data/lib/active_force/version.rb
CHANGED
data/lib/active_force.rb
CHANGED
|
@@ -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
|
|
@@ -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
|