active_force 0.20.1 → 0.21.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 +4 -4
- data/.github/workflows +51 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +11 -0
- data/README.md +11 -0
- data/lib/active_force/active_query.rb +34 -8
- data/lib/active_force/association/relation_model_builder.rb +5 -1
- data/lib/active_force/query.rb +12 -4
- data/lib/active_force/sobject.rb +51 -19
- data/lib/active_force/version.rb +1 -1
- data/lib/active_model/attribute/uninitialized_value.rb +12 -0
- data/lib/generators/active_force/model/model_generator.rb +1 -1
- data/spec/active_force/active_query_spec.rb +113 -5
- data/spec/active_force/association/relation_model_builder_spec.rb +30 -0
- data/spec/active_force/query_spec.rb +16 -0
- data/spec/active_force/sobject/includes_spec.rb +39 -0
- data/spec/active_force/sobject/select_spec.rb +40 -0
- data/spec/active_force/sobject_spec.rb +31 -2
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: acbf91e00afed35fd89a0dc1505ebb40d275883bca6aba9237dad7d554e10b7b
|
4
|
+
data.tar.gz: 488fe99179b48383967688ecbfc2c2f93f3f565ec177e972fb6e9ad5e68b5d8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b29a58711a0d6b63c82604c1bc2f3bfdc9b06e435468016fa7055bdd8ace5c779d09d4f11fdc32e7292ffd8c41a5784698610c1a6184116044bfa77b1c93cb5
|
7
|
+
data.tar.gz: 1bb0165385e5f06c7767d9463842e9daf742e11184e7518b008142162f8fb46bb2d35d5a1ba8f64be702cf169f5d9a6a9bfb3b8a1e83331fc11114c2ae30846a
|
data/.github/workflows
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
on:
|
2
|
+
pull_request:
|
3
|
+
types: [opened, reopened, synchronize]
|
4
|
+
pull_request_review_comment:
|
5
|
+
types: [created, edited, deleted]
|
6
|
+
|
7
|
+
name: Metomic Scan
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
scan-secrets:
|
11
|
+
name: Scan For Secrets
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
steps:
|
14
|
+
- name: checkout-repo
|
15
|
+
uses: actions/checkout@v3
|
16
|
+
with:
|
17
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
18
|
+
|
19
|
+
- name: authenticate-with-metomic
|
20
|
+
id: authenticate
|
21
|
+
continue-on-error: ${{ vars.METOMIC_FAIL_ON_CONNECTION_ERROR != 'TRUE' }}
|
22
|
+
run: |
|
23
|
+
curl -s --location --request GET '${{ vars.METOMIC_INTEGRATION_ENDPOINT }}/token' --header 'X-API-Key: ${{ secrets.METOMIC_API_TOKEN }}' -o .metomic_token.json
|
24
|
+
echo METOMIC_ACCESS_TOKEN=$(cat .metomic_token.json | jq -r -c '.accessToken') >> $GITHUB_OUTPUT
|
25
|
+
PASS=$(cat .metomic_token.json | jq -r -c '.accessToken // empty')
|
26
|
+
if [ -z "$PASS" ]; then
|
27
|
+
cat .metomic_token.json
|
28
|
+
echo ""
|
29
|
+
echo "FAIL: failed to fetch auth token from Metomic. Ensure required organisation variables / secrets are set correctly. METOMIC_INTEGRATION_ENDPOINT, METOMIC_API_TOKEN"
|
30
|
+
echo "The correct values for these variables are available from the installations page of the Metomic dashboard"
|
31
|
+
exit 1
|
32
|
+
fi
|
33
|
+
|
34
|
+
- name: checkout-metomic-action
|
35
|
+
id: checkout
|
36
|
+
uses: actions/checkout@v3
|
37
|
+
if: ${{ steps.authenticate.outcome == 'success' }}
|
38
|
+
continue-on-error: ${{ vars.METOMIC_FAIL_ON_CONNECTION_ERROR != 'TRUE' }}
|
39
|
+
with:
|
40
|
+
repository: metomic/metomic-github-integration-action.git
|
41
|
+
path: "./.metomic"
|
42
|
+
ref: "main"
|
43
|
+
token: ${{ steps.authenticate.outputs.METOMIC_ACCESS_TOKEN }}
|
44
|
+
|
45
|
+
- name: scan
|
46
|
+
uses: ./.metomic/.github/actions/scan-secrets
|
47
|
+
if: ${{ steps.authenticate.outputs.METOMIC_ACCESS_TOKEN && steps.checkout.outcome == 'success' }}
|
48
|
+
with:
|
49
|
+
metomic_endpoint: ${{ vars.METOMIC_INTEGRATION_ENDPOINT }}
|
50
|
+
metomic_api_token: ${{ secrets.METOMIC_API_TOKEN }}
|
51
|
+
head_ref: ${{ github.event.pull_request.head.sha }}
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,18 @@
|
|
2
2
|
|
3
3
|
## Not released
|
4
4
|
|
5
|
+
## 0.21.0
|
6
|
+
|
7
|
+
- Uninitialized attributes will ERROR instead of returning as `nil` (https://github.com/Beyond-Finance/active_force/pull/78)
|
8
|
+
- Add Range condition support (https://github.com/Beyond-Finance/active_force/pull/87)
|
9
|
+
- Use ':decimal' type for Salesforce currency fields in model generation (#88) (https://github.com/Beyond-Finance/active_force/pull/88)
|
10
|
+
- Fix includes with has_one associations when no associated record (https://github.com/Beyond-Finance/active_force/pull/83)
|
11
|
+
- Raise `UnknownFieldError` if `.where` is given non-existent attribute names (https://github.com/Beyond-Finance/active_force/pull/80)
|
12
|
+
- Fix `.update` and `.update!`: include given `nil` valued attributes in request (https://github.com/Beyond-Finance/active_force/pull/79)
|
13
|
+
- Change `.first` to not query the API if records have already been retrieved (https://github.com/Beyond-Finance/active_force/pull/77)
|
14
|
+
|
5
15
|
## 0.20.1
|
16
|
+
|
6
17
|
- Revert "ActiveForce .first performance enhancement (#73)" (https://github.com/Beyond-Finance/active_force/pull/76)
|
7
18
|
|
8
19
|
## 0.20.0
|
data/README.md
CHANGED
@@ -186,6 +186,17 @@ Account.where(contact_by: 'web').or(Account.where(contact_by: 'email'))
|
|
186
186
|
# OR (contact_by__c = 'email')"
|
187
187
|
```
|
188
188
|
|
189
|
+
You can use Ranges to specify comparisons:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
Account.where(last_activity_date: Date.new(2023, 1, 1)...Date.new(2024, 1, 1))
|
193
|
+
.where(annual_revenue: 1_000..)
|
194
|
+
#=> this will query "SELECT Id, Name...
|
195
|
+
# FROM Account
|
196
|
+
# WHERE (LastActivityDate >= 2023-01-01) AND (LastActivityDate < 2024-01-01)
|
197
|
+
# AND (AnnualRevenue >= 1000)
|
198
|
+
```
|
199
|
+
|
189
200
|
It is also possible to eager load associations:
|
190
201
|
|
191
202
|
```ruby
|
@@ -4,6 +4,13 @@ require 'forwardable'
|
|
4
4
|
|
5
5
|
module ActiveForce
|
6
6
|
class PreparedStatementInvalid < ArgumentError; end
|
7
|
+
|
8
|
+
class UnknownFieldError < StandardError
|
9
|
+
def initialize(object, field)
|
10
|
+
super("unknown field '#{field}' for #{object.name}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
7
14
|
class RecordNotFound < StandardError
|
8
15
|
attr_reader :table_name, :conditions
|
9
16
|
|
@@ -45,9 +52,9 @@ module ActiveForce
|
|
45
52
|
sfdc_client.query(super.to_s).first.expr0
|
46
53
|
end
|
47
54
|
|
48
|
-
def sum
|
55
|
+
def sum(field)
|
49
56
|
raise ArgumentError, 'field is required' if field.blank?
|
50
|
-
raise
|
57
|
+
raise UnknownFieldError.new(sobject, field) unless mappings.key?(field.to_sym)
|
51
58
|
|
52
59
|
sfdc_client.query(super(mappings.fetch(field.to_sym)).to_s).first.expr0
|
53
60
|
end
|
@@ -56,6 +63,10 @@ module ActiveForce
|
|
56
63
|
limit == 1 ? super.to_a.first : super
|
57
64
|
end
|
58
65
|
|
66
|
+
def first
|
67
|
+
super.to_a.first
|
68
|
+
end
|
69
|
+
|
59
70
|
def not args=nil, *rest
|
60
71
|
return self if args.nil?
|
61
72
|
|
@@ -162,16 +173,21 @@ module ActiveForce
|
|
162
173
|
end
|
163
174
|
|
164
175
|
def build_conditions_from_hash(hash)
|
165
|
-
hash.
|
166
|
-
|
176
|
+
hash.flat_map do |key, value|
|
177
|
+
field = mappings[key]
|
178
|
+
raise UnknownFieldError.new(sobject, key) if field.blank?
|
179
|
+
|
180
|
+
applicable_predicates(field, value)
|
167
181
|
end
|
168
182
|
end
|
169
183
|
|
170
|
-
def
|
171
|
-
if value.is_a?
|
172
|
-
in_predicate
|
184
|
+
def applicable_predicates(attribute, value)
|
185
|
+
if value.is_a?(Array)
|
186
|
+
[in_predicate(attribute, value)]
|
187
|
+
elsif value.is_a?(Range)
|
188
|
+
range_predicates(attribute, value)
|
173
189
|
else
|
174
|
-
eq_predicate
|
190
|
+
[eq_predicate(attribute, value)]
|
175
191
|
end
|
176
192
|
end
|
177
193
|
|
@@ -184,6 +200,16 @@ module ActiveForce
|
|
184
200
|
"#{attribute} = #{enclose_value value}"
|
185
201
|
end
|
186
202
|
|
203
|
+
def range_predicates(attribute, range)
|
204
|
+
conditions = []
|
205
|
+
conditions << "#{attribute} >= #{enclose_value(range.begin)}" unless range.begin.nil?
|
206
|
+
unless range.end.nil?
|
207
|
+
operator = range.exclude_end? ? '<' : '<='
|
208
|
+
conditions << "#{attribute} #{operator} #{enclose_value(range.end)}"
|
209
|
+
end
|
210
|
+
conditions
|
211
|
+
end
|
212
|
+
|
187
213
|
def enclose_value value
|
188
214
|
case value
|
189
215
|
when String
|
@@ -50,7 +50,11 @@ module ActiveForce
|
|
50
50
|
|
51
51
|
class BuildFromArray < AbstractBuildFrom
|
52
52
|
def call
|
53
|
-
|
53
|
+
if association.is_a?(HasOneAssociation)
|
54
|
+
association.build(value.first, association_mapping)
|
55
|
+
else
|
56
|
+
value.map { |mash| association.build(mash, association_mapping) }
|
57
|
+
end
|
54
58
|
end
|
55
59
|
end
|
56
60
|
|
data/lib/active_force/query.rb
CHANGED
@@ -78,7 +78,15 @@ module ActiveForce
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def first
|
81
|
-
|
81
|
+
if @records
|
82
|
+
clone_and_set_instance_variables(
|
83
|
+
size: 1,
|
84
|
+
records: [@records.first],
|
85
|
+
decorated_records: [@decorated_records&.first]
|
86
|
+
)
|
87
|
+
else
|
88
|
+
clone_and_set_instance_variables(size: 1)
|
89
|
+
end
|
82
90
|
end
|
83
91
|
|
84
92
|
def last(limit = 1)
|
@@ -126,9 +134,9 @@ module ActiveForce
|
|
126
134
|
|
127
135
|
def clone_and_set_instance_variables instance_variable_hash={}
|
128
136
|
clone = self.clone
|
129
|
-
|
130
|
-
|
131
|
-
|
137
|
+
{ decorated_records: nil, records: nil }
|
138
|
+
.merge(instance_variable_hash)
|
139
|
+
.each { |k,v| clone.instance_variable_set("@#{k.to_s}", v) }
|
132
140
|
clone
|
133
141
|
end
|
134
142
|
end
|
data/lib/active_force/sobject.rb
CHANGED
@@ -7,6 +7,7 @@ require 'yaml'
|
|
7
7
|
require 'forwardable'
|
8
8
|
require 'logger'
|
9
9
|
require 'restforce'
|
10
|
+
require 'active_model/attribute/uninitialized_value'
|
10
11
|
|
11
12
|
module ActiveForce
|
12
13
|
class RecordInvalid < StandardError;end
|
@@ -34,8 +35,24 @@ module ActiveForce
|
|
34
35
|
def_delegators :query, :not, :or, :where, :first, :last, :all, :find, :find!, :find_by, :find_by!, :sum, :count, :includes, :limit, :order, :select, :none
|
35
36
|
def_delegators :mapping, :table, :table_name, :custom_table?, :mappings
|
36
37
|
|
38
|
+
def update(id, attributes)
|
39
|
+
prepare_for_update(id, attributes).update
|
40
|
+
end
|
41
|
+
|
42
|
+
def update!(id, attributes)
|
43
|
+
prepare_for_update(id, attributes).update!
|
44
|
+
end
|
45
|
+
|
37
46
|
private
|
38
47
|
|
48
|
+
def prepare_for_update(id, attributes)
|
49
|
+
new(attributes.merge(id: id)).tap do |obj|
|
50
|
+
attributes.each do |name, value|
|
51
|
+
obj.public_send("#{name}_will_change!") if value.nil?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
39
56
|
###
|
40
57
|
# Provide each subclass with a default id field. Can be overridden
|
41
58
|
# in the subclass if needed
|
@@ -64,6 +81,9 @@ module ActiveForce
|
|
64
81
|
def self.build mash, association_mapping={}
|
65
82
|
return unless mash
|
66
83
|
sobject = new
|
84
|
+
|
85
|
+
attributes_not_selected = sobject.class.fields.reject{|key| mash.keys.include?(key)}
|
86
|
+
sobject.uninitialize_attributes(attributes_not_selected)
|
67
87
|
sobject.build_attributes = mash[:build_attributes] || mash
|
68
88
|
sobject.run_callbacks(:build) do
|
69
89
|
mash.each do |column, value|
|
@@ -110,6 +130,17 @@ module ActiveForce
|
|
110
130
|
self
|
111
131
|
end
|
112
132
|
|
133
|
+
def uninitialize_attributes(attrs)
|
134
|
+
return if attrs.blank?
|
135
|
+
self.instance_variable_get(:@attributes).instance_variable_get(:@attributes).each do |key, value|
|
136
|
+
if attrs.include?(self.mappings.dig(value.name.to_sym))
|
137
|
+
self.instance_variable_get(:@attributes).instance_variable_get(:@attributes)[key] = ActiveModel::Attribute::UninitializedValue.new(value.name, value.type)
|
138
|
+
else
|
139
|
+
key
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
113
144
|
def create
|
114
145
|
create!
|
115
146
|
rescue Faraday::ClientError, RecordInvalid => error
|
@@ -131,14 +162,6 @@ module ActiveForce
|
|
131
162
|
new(args).create!
|
132
163
|
end
|
133
164
|
|
134
|
-
def self.update(id, attributes)
|
135
|
-
new(attributes.merge(id: id)).update
|
136
|
-
end
|
137
|
-
|
138
|
-
def self.update!(id, attributes)
|
139
|
-
new(attributes.merge(id: id)).update!
|
140
|
-
end
|
141
|
-
|
142
165
|
def save!
|
143
166
|
run_callbacks :save do
|
144
167
|
if persisted?
|
@@ -183,18 +206,12 @@ module ActiveForce
|
|
183
206
|
self
|
184
207
|
end
|
185
208
|
|
186
|
-
def write_value
|
187
|
-
if association = self.class.find_association(key.to_sym)
|
188
|
-
|
189
|
-
value = Association::RelationModelBuilder.build(association, value, association_mapping)
|
190
|
-
elsif key.to_sym.in?(mappings.keys)
|
191
|
-
# key is a field name
|
192
|
-
field = key
|
209
|
+
def write_value(key, value, association_mapping = {})
|
210
|
+
if (association = self.class.find_association(key.to_sym))
|
211
|
+
write_association_value(association, value, association_mapping)
|
193
212
|
else
|
194
|
-
|
195
|
-
field = mappings.key(key)
|
213
|
+
write_field_value(key, value)
|
196
214
|
end
|
197
|
-
send "#{field}=", value if field && respond_to?(field)
|
198
215
|
end
|
199
216
|
|
200
217
|
def [](name)
|
@@ -205,7 +222,7 @@ module ActiveForce
|
|
205
222
|
send("#{name.to_sym}=", value)
|
206
223
|
end
|
207
224
|
|
208
|
-
|
225
|
+
private
|
209
226
|
|
210
227
|
def validate!
|
211
228
|
unless valid?
|
@@ -215,6 +232,21 @@ module ActiveForce
|
|
215
232
|
end
|
216
233
|
end
|
217
234
|
|
235
|
+
def write_association_value(association, value, association_mapping)
|
236
|
+
association_cache[association.relation_name] = Association::RelationModelBuilder.build(association, value,
|
237
|
+
association_mapping)
|
238
|
+
end
|
239
|
+
|
240
|
+
def write_field_value(field_key, value)
|
241
|
+
field = if mappings.key?(field_key.to_sym)
|
242
|
+
field_key
|
243
|
+
else
|
244
|
+
mappings.key(field_key)
|
245
|
+
end
|
246
|
+
|
247
|
+
send("#{field}=", value) if field && respond_to?(field)
|
248
|
+
end
|
249
|
+
|
218
250
|
def handle_save_error error
|
219
251
|
return false if error.class == RecordInvalid
|
220
252
|
logger_output __method__, error, attributes
|
data/lib/active_force/version.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
class TestSObject < ActiveForce::SObject
|
4
|
+
def self.decorate(records)
|
5
|
+
records
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
3
9
|
describe ActiveForce::ActiveQuery do
|
4
10
|
let(:sobject) do
|
5
|
-
|
6
|
-
|
7
|
-
fields: [],
|
8
|
-
|
9
|
-
})
|
11
|
+
class_double(
|
12
|
+
TestSObject,
|
13
|
+
{ table_name: 'table_name', fields: [], mappings: mappings, name: 'TableName' }
|
14
|
+
)
|
10
15
|
end
|
11
16
|
let(:mappings){ { id: "Id", field: "Field__c", other_field: "Other_Field" } }
|
12
17
|
let(:client) { double('client', query: nil) }
|
@@ -148,6 +153,74 @@ describe ActiveForce::ActiveQuery do
|
|
148
153
|
expect(new_query.to_s).to end_with("(Field__c = NULL)")
|
149
154
|
end
|
150
155
|
|
156
|
+
describe 'range filter' do
|
157
|
+
def check_endless(query, start, field: 'Field__c')
|
158
|
+
expect(query.to_s).to end_with("(#{field} >= #{start})")
|
159
|
+
end
|
160
|
+
|
161
|
+
def check_beginless_inclusive(query, finish, field: 'Field__c')
|
162
|
+
expect(query.to_s).to end_with("(#{field} <= #{finish})")
|
163
|
+
end
|
164
|
+
|
165
|
+
def check_beginless_exclusive(query, finish, field: 'Field__c')
|
166
|
+
expect(query.to_s).to end_with("(#{field} < #{finish})")
|
167
|
+
end
|
168
|
+
|
169
|
+
def check_inclusive(query, start, finish, field: 'Field__c')
|
170
|
+
expect(query.to_s).to end_with("(#{field} >= #{start}) AND (#{field} <= #{finish})")
|
171
|
+
end
|
172
|
+
|
173
|
+
def check_exclusive(query, start, finish, field: 'Field__c')
|
174
|
+
expect(query.to_s).to end_with("(#{field} >= #{start}) AND (#{field} < #{finish})")
|
175
|
+
end
|
176
|
+
|
177
|
+
def check_ranges(base_query, start, finish, &format_block)
|
178
|
+
formatted_start = format_block&.call(start) || start.to_s
|
179
|
+
formatted_finish = format_block&.call(finish) || finish.to_s
|
180
|
+
check_endless(base_query.where(field: start..), formatted_start)
|
181
|
+
check_beginless_inclusive(base_query.where(field: ..finish), formatted_finish)
|
182
|
+
check_beginless_exclusive(base_query.where(field: ...finish), formatted_finish)
|
183
|
+
check_inclusive(base_query.where(field: start..finish), formatted_start, formatted_finish)
|
184
|
+
check_exclusive(base_query.where(field: start...finish), formatted_start, formatted_finish)
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'renders with Dates' do
|
188
|
+
check_ranges(active_query, Date.new(2024, 2, 2), Date.new(2024, 2, 28))
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'renders with DateTimes' do
|
192
|
+
check_ranges(active_query, DateTime.new(2024, 1, 31, 1, 2, 3), DateTime.new(2024, 1, 31, 1, 2, 4))
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'renders with Times' do
|
196
|
+
check_ranges(active_query, Time.current, Time.current + 1.hour, &:iso8601)
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'renders with Strings' do
|
200
|
+
check_ranges(active_query, 'a', 'z') { |x| "'#{x}'"}
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'renders with Integers' do
|
204
|
+
check_ranges(active_query, 1, 99)
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'renders with Floats' do
|
208
|
+
check_ranges(active_query, 0.5, 100.89)
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'renders with BigDecimal' do
|
212
|
+
check_ranges(active_query, BigDecimal('0.888'), BigDecimal('11.0003'))
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'composes with other conditions' do
|
216
|
+
query = active_query.where(id: 'id1'.., field: 1..99, other_field: 'a')
|
217
|
+
.not(id: 'id2')
|
218
|
+
expect(query.to_s).to end_with(
|
219
|
+
"(Id >= 'id1') AND (Field__c >= 1) AND (Field__c <= 99) AND (Other_Field = 'a') AND (NOT ((Id = 'id2')))"
|
220
|
+
)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
151
224
|
describe 'bind parameters' do
|
152
225
|
let(:mappings) do
|
153
226
|
super().merge({
|
@@ -296,6 +369,13 @@ describe ActiveForce::ActiveQuery do
|
|
296
369
|
end
|
297
370
|
end
|
298
371
|
end
|
372
|
+
|
373
|
+
context 'when given attributes Hash with fields that do not exist on the SObject' do
|
374
|
+
it 'raises UnknownFieldError' do
|
375
|
+
expect { active_query.where(xyz: 1) }
|
376
|
+
.to raise_error(ActiveForce::UnknownFieldError, /unknown field 'xyz' for #{sobject.name}/i)
|
377
|
+
end
|
378
|
+
end
|
299
379
|
end
|
300
380
|
|
301
381
|
describe '#not' do
|
@@ -329,6 +409,11 @@ describe ActiveForce::ActiveQuery do
|
|
329
409
|
new_query = active_query.find_by field: 123
|
330
410
|
expect(new_query).to be_nil
|
331
411
|
end
|
412
|
+
|
413
|
+
it 'should raise UnknownFieldError if given invalid field' do
|
414
|
+
expect { active_query.find_by(invalid: true) }
|
415
|
+
.to raise_error(ActiveForce::UnknownFieldError, /unknown field 'invalid' for #{sobject.name}/i)
|
416
|
+
end
|
332
417
|
end
|
333
418
|
|
334
419
|
describe '#find_by!' do
|
@@ -337,6 +422,11 @@ describe ActiveForce::ActiveQuery do
|
|
337
422
|
expect { active_query.find_by!(field: 123) }
|
338
423
|
.to raise_error(ActiveForce::RecordNotFound, "Couldn't find #{sobject.table_name} with {:field=>123}")
|
339
424
|
end
|
425
|
+
|
426
|
+
it 'should raise UnknownFieldError if given invalid field' do
|
427
|
+
expect { active_query.find_by!(invalid: true) }
|
428
|
+
.to raise_error(ActiveForce::UnknownFieldError, /unknown field 'invalid' for #{sobject.name}/i)
|
429
|
+
end
|
340
430
|
end
|
341
431
|
|
342
432
|
describe '#find!' do
|
@@ -463,4 +553,22 @@ describe ActiveForce::ActiveQuery do
|
|
463
553
|
end
|
464
554
|
|
465
555
|
end
|
556
|
+
|
557
|
+
describe '#first' do
|
558
|
+
before do
|
559
|
+
allow(client).to receive(:query).and_return(api_result)
|
560
|
+
api_result.each do |instance|
|
561
|
+
allow(active_query).to receive(:build).with(instance, {}).and_return(double(:sobject, id: instance['Id']))
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
it 'returns a single record when the api was already queried' do
|
566
|
+
active_query.to_a # this will simulate the api call as to_a executes the query and populates the records
|
567
|
+
expect(active_query.first.id).to eq("0000000000AAAAABBB")
|
568
|
+
end
|
569
|
+
|
570
|
+
it 'returns a single record when the api was not already queried' do
|
571
|
+
expect(active_query.first.id).to eq("0000000000AAAAABBB")
|
572
|
+
end
|
573
|
+
end
|
466
574
|
end
|
@@ -70,6 +70,36 @@ module ActiveForce
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
+
context 'with a restforce collection value' do
|
74
|
+
let(:value) do
|
75
|
+
build_restforce_collection([
|
76
|
+
build_restforce_sobject('Id' => 'first'),
|
77
|
+
build_restforce_sobject('Id' => 'second')
|
78
|
+
])
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns a child for the first value' do
|
82
|
+
actual = instance.build_relation_model
|
83
|
+
expect(actual).to be_a(HasOneChild)
|
84
|
+
expect(actual.id).to eq('first')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with an array value' do
|
89
|
+
let(:value) do
|
90
|
+
[
|
91
|
+
build_restforce_sobject('Id' => 'first'),
|
92
|
+
build_restforce_sobject('Id' => 'second')
|
93
|
+
]
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'returns a child for the first value' do
|
97
|
+
actual = instance.build_relation_model
|
98
|
+
expect(actual).to be_a(HasOneChild)
|
99
|
+
expect(actual.id).to eq('first')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
73
103
|
context 'without a value' do
|
74
104
|
let(:value){ nil }
|
75
105
|
|
@@ -165,6 +165,22 @@ describe ActiveForce::Query do
|
|
165
165
|
expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
|
166
166
|
expect(new_query.to_s).to eq 'SELECT Id, name, etc FROM table_name LIMIT 1'
|
167
167
|
end
|
168
|
+
|
169
|
+
it "does not query if records have already been fetched" do
|
170
|
+
query = ActiveForce::Query.new 'table_name'
|
171
|
+
query.instance_variable_set(:@records, %w[foo bar])
|
172
|
+
query.instance_variable_set(:@decorated_records, %w[foo bar])
|
173
|
+
expect(query).not_to receive(:clone_and_set_instance_variables).with(size: 1)
|
174
|
+
expect(query).to receive(:clone_and_set_instance_variables).with(size: 1, records: ['foo'], decorated_records: ['foo'])
|
175
|
+
query.first
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'queries the api if it has not been queried yet' do
|
179
|
+
query = ActiveForce::Query.new 'table_name'
|
180
|
+
query.instance_variable_set(:@records, nil)
|
181
|
+
expect(query).to receive(:clone_and_set_instance_variables).with(size: 1)
|
182
|
+
query.first
|
183
|
+
end
|
168
184
|
end
|
169
185
|
|
170
186
|
describe '.last' do
|
@@ -301,6 +301,45 @@ module ActiveForce
|
|
301
301
|
end
|
302
302
|
end
|
303
303
|
|
304
|
+
context 'when query returns nil for associated record' do
|
305
|
+
let(:response) do
|
306
|
+
[build_restforce_sobject({ 'Id' => '123', 'Membership__r' => nil })]
|
307
|
+
end
|
308
|
+
|
309
|
+
before do
|
310
|
+
allow(client).to receive(:query).and_return(response)
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'the association method returns nil without making another request' do
|
314
|
+
member = ClubMember.includes(:membership).where(id: '123').first
|
315
|
+
membership = member.membership
|
316
|
+
expect(membership).to be_nil
|
317
|
+
expect(client).to have_received(:query).once
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
context 'when query returns an associated record' do
|
323
|
+
let(:response) do
|
324
|
+
[
|
325
|
+
build_restforce_sobject(
|
326
|
+
{
|
327
|
+
'Id' => '123',
|
328
|
+
'Membership__r' => build_restforce_collection([build_restforce_sobject({ 'Id' => '33' })])
|
329
|
+
}
|
330
|
+
)
|
331
|
+
]
|
332
|
+
end
|
333
|
+
|
334
|
+
before do
|
335
|
+
allow(client).to receive(:query).and_return(response)
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'the association method returns the record without making another request' do
|
339
|
+
member = ClubMember.includes(:membership).where(id: '123').first
|
340
|
+
expect(member.membership.id).to eq('33')
|
341
|
+
expect(client).to have_received(:query).once
|
342
|
+
end
|
304
343
|
end
|
305
344
|
|
306
345
|
context 'when invalid associations are passed' do
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ActiveForce
|
4
|
+
describe SObject do
|
5
|
+
let(:client){ double "client" }
|
6
|
+
|
7
|
+
before do
|
8
|
+
ActiveForce.sfdc_client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '.select' do
|
12
|
+
it 'has correct fields in query' do
|
13
|
+
query = Territory.select(:name, :id)
|
14
|
+
expect(query.fields).to eq(["Name", "Id"])
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'when getting the value of an uninitialized attribute' do
|
18
|
+
let(:territory) { Territory.select(:id).first }
|
19
|
+
let(:response) do
|
20
|
+
[build_restforce_sobject({
|
21
|
+
"Id" => "123",
|
22
|
+
"Quota__c" => "321",
|
23
|
+
})]
|
24
|
+
end
|
25
|
+
|
26
|
+
before do
|
27
|
+
allow(client).to receive(:query).once.and_return response
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'raises missing attribute error if uninitialized variable is called' do
|
31
|
+
expect{territory.name}.to raise_error(ActiveModel::MissingAttributeError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns SObjects with Uninitialized Value' do
|
35
|
+
expect(territory.instance_variable_get(:@attributes)["name"]).to be_an_instance_of(ActiveModel::Attribute::UninitializedValue)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -321,6 +321,15 @@ describe ActiveForce::SObject do
|
|
321
321
|
.and_return(true)
|
322
322
|
Whizbang.update('12345678', text: 'my text')
|
323
323
|
end
|
324
|
+
|
325
|
+
it 'includes given nil values in the request' do
|
326
|
+
allow(client).to receive(:update!).and_return(true)
|
327
|
+
Whizbang.update('test123', text: nil, date: nil)
|
328
|
+
expect(client).to have_received(:update!).with(
|
329
|
+
Whizbang.table_name,
|
330
|
+
{ 'Id' => 'test123', 'Text_Label' => nil, 'Date_Label' => nil, 'Updated_From__c' => 'Rails' }
|
331
|
+
)
|
332
|
+
end
|
324
333
|
end
|
325
334
|
|
326
335
|
describe 'self.update!' do
|
@@ -330,6 +339,15 @@ describe ActiveForce::SObject do
|
|
330
339
|
.and_return(true)
|
331
340
|
Whizbang.update('123456789', text: 'some other text')
|
332
341
|
end
|
342
|
+
|
343
|
+
it 'includes given nil values in the request' do
|
344
|
+
allow(client).to receive(:update!).and_return(true)
|
345
|
+
Whizbang.update!('test123', text: nil, date: nil)
|
346
|
+
expect(client).to have_received(:update!).with(
|
347
|
+
Whizbang.table_name,
|
348
|
+
{ 'Id' => 'test123', 'Text_Label' => nil, 'Date_Label' => nil, 'Updated_From__c' => 'Rails' }
|
349
|
+
)
|
350
|
+
end
|
333
351
|
end
|
334
352
|
end
|
335
353
|
|
@@ -368,9 +386,9 @@ describe ActiveForce::SObject do
|
|
368
386
|
expect { Whizbang.sum(nil) }.to raise_error(ArgumentError, 'field is required')
|
369
387
|
end
|
370
388
|
|
371
|
-
it 'raises
|
389
|
+
it 'raises UnknownFieldError if given invalid field' do
|
372
390
|
expect { Whizbang.sum(:invalid) }
|
373
|
-
.to raise_error(
|
391
|
+
.to raise_error(ActiveForce::UnknownFieldError, /unknown field 'invalid' for Whizbang/i)
|
374
392
|
end
|
375
393
|
|
376
394
|
it 'sends the correct query to the client' do
|
@@ -401,6 +419,11 @@ describe ActiveForce::SObject do
|
|
401
419
|
expect(client).to receive(:query).with("SELECT #{Whizbang.fields.join ', '} FROM Whizbang__c WHERE (Id = 123) AND (Text_Label = 'foo') LIMIT 1")
|
402
420
|
Whizbang.find_by id: 123, text: "foo"
|
403
421
|
end
|
422
|
+
|
423
|
+
it 'raises UnknownFieldError if given invalid field' do
|
424
|
+
expect { Whizbang.find_by(xyz: 1) }
|
425
|
+
.to raise_error(ActiveForce::UnknownFieldError, /unknown field 'xyz' for Whizbang/)
|
426
|
+
end
|
404
427
|
end
|
405
428
|
|
406
429
|
describe "#find_by!" do
|
@@ -408,10 +431,16 @@ describe ActiveForce::SObject do
|
|
408
431
|
expect(client).to receive(:query).with("SELECT #{Whizbang.fields.join ', '} FROM Whizbang__c WHERE (Id = 123) AND (Text_Label = 'foo') LIMIT 1").and_return([Restforce::Mash.new(Id: 123, text: 'foo')])
|
409
432
|
Whizbang.find_by! id: 123, text: "foo"
|
410
433
|
end
|
434
|
+
|
411
435
|
it "raises if nothing found" do
|
412
436
|
expect(client).to receive(:query).with("SELECT #{Whizbang.fields.join ', '} FROM Whizbang__c WHERE (Id = 123) AND (Text_Label = 'foo') LIMIT 1")
|
413
437
|
expect { Whizbang.find_by! id: 123, text: "foo" }.to raise_error(ActiveForce::RecordNotFound)
|
414
438
|
end
|
439
|
+
|
440
|
+
it 'raises UnknownFieldError if given invalid field' do
|
441
|
+
expect { Whizbang.find_by!(xyz: 1) }
|
442
|
+
.to raise_error(ActiveForce::UnknownFieldError, /unknown field 'xyz' for Whizbang/)
|
443
|
+
end
|
415
444
|
end
|
416
445
|
|
417
446
|
describe '.find!' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_force
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.21.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eloy Espinaco
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2024-02-15 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activemodel
|
@@ -134,6 +134,7 @@ files:
|
|
134
134
|
- ".circleci/config.yml"
|
135
135
|
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
136
136
|
- ".github/ISSUE_TEMPLATE/feature_request.md"
|
137
|
+
- ".github/workflows"
|
137
138
|
- ".gitignore"
|
138
139
|
- ".mailmap"
|
139
140
|
- ".rspec"
|
@@ -166,6 +167,7 @@ files:
|
|
166
167
|
- lib/active_force/standard_types.rb
|
167
168
|
- lib/active_force/table.rb
|
168
169
|
- lib/active_force/version.rb
|
170
|
+
- lib/active_model/attribute/uninitialized_value.rb
|
169
171
|
- lib/active_model/type/salesforce/multipicklist.rb
|
170
172
|
- lib/active_model/type/salesforce/percent.rb
|
171
173
|
- lib/generators/active_force/model/USAGE
|
@@ -183,6 +185,7 @@ files:
|
|
183
185
|
- spec/active_force/mapping_spec.rb
|
184
186
|
- spec/active_force/query_spec.rb
|
185
187
|
- spec/active_force/sobject/includes_spec.rb
|
188
|
+
- spec/active_force/sobject/select_spec.rb
|
186
189
|
- spec/active_force/sobject/table_name_spec.rb
|
187
190
|
- spec/active_force/sobject_spec.rb
|
188
191
|
- spec/active_force/table_spec.rb
|
@@ -216,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
216
219
|
- !ruby/object:Gem::Version
|
217
220
|
version: '0'
|
218
221
|
requirements: []
|
219
|
-
rubygems_version: 3.
|
222
|
+
rubygems_version: 3.4.10
|
220
223
|
signing_key:
|
221
224
|
specification_version: 4
|
222
225
|
summary: Help you implement models persisting on Sales Force within Rails using RESTForce
|
@@ -233,6 +236,7 @@ test_files:
|
|
233
236
|
- spec/active_force/mapping_spec.rb
|
234
237
|
- spec/active_force/query_spec.rb
|
235
238
|
- spec/active_force/sobject/includes_spec.rb
|
239
|
+
- spec/active_force/sobject/select_spec.rb
|
236
240
|
- spec/active_force/sobject/table_name_spec.rb
|
237
241
|
- spec/active_force/sobject_spec.rb
|
238
242
|
- spec/active_force/table_spec.rb
|