active_force 0.17.0 → 0.19.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/CHANGELOG.md +11 -0
- data/README.md +20 -3
- data/lib/active_force/active_query.rb +2 -2
- data/lib/active_force/association/association.rb +8 -0
- data/lib/active_force/association/eager_load_projection_builder.rb +12 -4
- data/lib/active_force/bulk/job.rb +136 -0
- data/lib/active_force/bulk/job_result.rb +48 -0
- data/lib/active_force/bulk/records.rb +25 -0
- data/lib/active_force/bulk.rb +47 -0
- data/lib/active_force/query.rb +2 -2
- data/lib/active_force/sobject.rb +11 -1
- data/lib/active_force/version.rb +1 -1
- data/lib/active_force.rb +1 -0
- data/spec/active_force/active_query_spec.rb +66 -1
- data/spec/active_force/bulk/job_result_spec.rb +116 -0
- data/spec/active_force/bulk/job_spec.rb +113 -0
- data/spec/active_force/bulk/record_spec.rb +32 -0
- data/spec/active_force/bulk_spec.rb +110 -0
- data/spec/active_force/query_spec.rb +24 -8
- data/spec/active_force/sobject/includes_spec.rb +26 -2
- data/spec/active_force/sobject_spec.rb +18 -0
- metadata +18 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38ef881c292f8ecb12be07a0471f759a06b9155af5e3a49c26dbb69e024db87f
|
4
|
+
data.tar.gz: 6a209feb04dc0c6a192c084a95c2e55c64696bbb54636496fc98dc8c56c7b653
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 814f0f4844f94acea113a4eabf1e3afd366b2b00255fe5bcca5ccfccb0f5d862651548302373d078a7c070422d6db6c8cf971f33112c360f0d43f133d500c98a
|
7
|
+
data.tar.gz: 48f595e34749ec49adeaeb8320c407911f8841d83fcd433cdd5e7e9bc5b6b8f37d01a0f30f6a96610d91949ab8e30e2cb4b0c3f9aa18692447fc31ed6d4b75fb
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,17 @@
|
|
2
2
|
|
3
3
|
## Not released
|
4
4
|
|
5
|
+
## 0.19.0
|
6
|
+
|
7
|
+
- Bulk API methods. (https://github.com/Beyond-Finance/active_force/pull/65)
|
8
|
+
|
9
|
+
## 0.18.0
|
10
|
+
|
11
|
+
- Fix eager loading of scoped associations. (https://github.com/Beyond-Finance/active_force/pull/67)
|
12
|
+
- Adding `.blank?`, `.present?`, and `.any?` delegators to `ActiveQuery`. (https://github.com/Beyond-Finance/active_force/pull/68)
|
13
|
+
- Adding `update` and `update!` class methods on `SObject`. (https://github.com/Beyond-Finance/active_force/pull/66)
|
14
|
+
- Allow an argument to `last` allowing the query to select the `last(n)` records. Default is 1. (https://github.com/Beyond-Finance/active_force/pull/66)
|
15
|
+
|
5
16
|
## 0.17.0
|
6
17
|
|
7
18
|
- Fix bug with has_many queries due to query method chaining mutating in-place (https://github.com/Beyond-Finance/active_force/pull/10)
|
data/README.md
CHANGED
@@ -194,7 +194,7 @@ Comment.includes(:post)
|
|
194
194
|
|
195
195
|
It is possible to eager load multi level associations
|
196
196
|
|
197
|
-
In order to utilize multi level eager loads, the API version should be set to 58.0 or higher when instantiating a Restforce client
|
197
|
+
In order to utilize multi level eager loads, the API version should be set to 58.0 or higher when instantiating a Restforce client
|
198
198
|
|
199
199
|
```ruby
|
200
200
|
Restforce.new({api_version: '58.0'})
|
@@ -212,8 +212,8 @@ Comment.includes({post: {owner: :account}})
|
|
212
212
|
Summing the values of a column:
|
213
213
|
```ruby
|
214
214
|
Transaction.where(offer_id: 'ABD832024').sum(:amount)
|
215
|
-
#=> This will query "SELECT SUM(Amount__c)
|
216
|
-
# FROM Transaction__c
|
215
|
+
#=> This will query "SELECT SUM(Amount__c)
|
216
|
+
# FROM Transaction__c
|
217
217
|
# WHERE offer_id = 'ABD832024'"
|
218
218
|
```
|
219
219
|
|
@@ -249,6 +249,22 @@ accounts = Account.where(web_enabled: 1).limit(2)
|
|
249
249
|
with data from another API, and will only query the other API once.
|
250
250
|
```
|
251
251
|
|
252
|
+
### Bulk Jobs
|
253
|
+
|
254
|
+
For more information about usage and limits of the Salesforce Bulk API see the [docs][4].
|
255
|
+
|
256
|
+
Convenience class methods have been added to `ActiveForce::SObject` to make it possible to utilize the Salesforce Bulk API v2.0.
|
257
|
+
The methods are: `bulk_insert_all`, `bulk_update_all`, & `bulk_delete_all`. They all expect input as an Array of attributes as a Hash:
|
258
|
+
```ruby
|
259
|
+
[
|
260
|
+
{ id: '11111111', attribute1: 'value1', attribute2: 'value2'},
|
261
|
+
{ id: '22222222', attribute1: 'value3', attribute2: 'value4'},
|
262
|
+
]
|
263
|
+
```
|
264
|
+
The attributes will be mapped back to what's expected on the SF side automatically. The response is a `ActiveForce::Bulk::JobResult` object
|
265
|
+
which can access `successful` & `failed` results, has some `stats`, and the original `job` object that was used to create and process the
|
266
|
+
Bulk job.
|
267
|
+
|
252
268
|
### Model generator
|
253
269
|
|
254
270
|
When using rails, you can generate a model with all the fields you have on your SFDC table by running:
|
@@ -268,4 +284,5 @@ When using rails, you can generate a model with all the fields you have on your
|
|
268
284
|
[1]: http://www.salesforce.com
|
269
285
|
[2]: https://github.com/ejholmes/restforce
|
270
286
|
[3]: https://github.com/bkeepers/dotenv
|
287
|
+
[4]: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm
|
271
288
|
|
@@ -21,9 +21,9 @@ module ActiveForce
|
|
21
21
|
attr_reader :sobject, :association_mapping, :belongs_to_association_mapping
|
22
22
|
|
23
23
|
def_delegators :sobject, :sfdc_client, :build, :table_name, :mappings
|
24
|
-
def_delegators :to_a, :each, :map, :inspect, :pluck, :each_with_object
|
24
|
+
def_delegators :to_a, :blank?, :present?, :any?, :each, :map, :inspect, :pluck, :each_with_object
|
25
25
|
|
26
|
-
def initialize
|
26
|
+
def initialize(sobject, custom_table_name = nil)
|
27
27
|
@sobject = sobject
|
28
28
|
@association_mapping = {}
|
29
29
|
@belongs_to_association_mapping = {}
|
@@ -26,6 +26,14 @@ module ActiveForce
|
|
26
26
|
options[:relationship_name] || relation_model.to_s.constantize.table_name
|
27
27
|
end
|
28
28
|
|
29
|
+
def scoped_as
|
30
|
+
options[:scoped_as] || nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def scoped?
|
34
|
+
options[:scoped_as].present?
|
35
|
+
end
|
36
|
+
|
29
37
|
###
|
30
38
|
# Does this association's relation_model represent
|
31
39
|
# +sfdc_table_name+? Examples of +sfdc_table_name+
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module ActiveForce
|
2
2
|
module Association
|
3
|
+
class InvalidEagerLoadAssociation < StandardError; end
|
3
4
|
class EagerLoadProjectionBuilder
|
4
5
|
class << self
|
5
6
|
def build(association, parent_association_field = nil)
|
@@ -34,6 +35,13 @@ module ActiveForce
|
|
34
35
|
def projections
|
35
36
|
raise "Must define #{self.class.name}#projections"
|
36
37
|
end
|
38
|
+
|
39
|
+
def apply_association_scope(query)
|
40
|
+
return query unless association.scoped?
|
41
|
+
raise InvalidEagerLoadAssociation, "Cannot use scopes that expect arguments: #{association.relation_name}" if association.scoped_as.arity.positive?
|
42
|
+
|
43
|
+
query.instance_exec(&association.scoped_as)
|
44
|
+
end
|
37
45
|
end
|
38
46
|
|
39
47
|
class HasManyAssociationProjectionBuilder < AbstractProjectionBuilder
|
@@ -43,17 +51,17 @@ module ActiveForce
|
|
43
51
|
# to be pluralized
|
44
52
|
def projections
|
45
53
|
relationship_name = association.sfdc_association_field
|
46
|
-
query =
|
54
|
+
query = ActiveQuery.new(association.relation_model, relationship_name)
|
47
55
|
query.fields association.relation_model.fields
|
48
|
-
["(#{query.to_s})"]
|
56
|
+
["(#{apply_association_scope(query).to_s})"]
|
49
57
|
end
|
50
58
|
end
|
51
59
|
|
52
60
|
class HasOneAssociationProjectionBuilder < AbstractProjectionBuilder
|
53
61
|
def projections
|
54
|
-
query =
|
62
|
+
query = ActiveQuery.new(association.relation_model, association.sfdc_association_field)
|
55
63
|
query.fields association.relation_model.fields
|
56
|
-
["(#{query.to_s})"]
|
64
|
+
["(#{apply_association_scope(query).to_s})"]
|
57
65
|
end
|
58
66
|
end
|
59
67
|
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'active_force/bulk/job_result'
|
2
|
+
|
3
|
+
module ActiveForce
|
4
|
+
module Bulk
|
5
|
+
class Job
|
6
|
+
attr_reader :operation, :records, :state, :object, :content_url
|
7
|
+
attr_accessor :id
|
8
|
+
|
9
|
+
STATES = {
|
10
|
+
Open: 'Open',
|
11
|
+
UploadComplete: 'UploadComplete',
|
12
|
+
InProgress: 'InProgress',
|
13
|
+
JobComplete: 'JobComplete',
|
14
|
+
Failed: 'Failed',
|
15
|
+
Aborted: 'Aborted',
|
16
|
+
Deleted: 'Deleted'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
OPERATIONS = %i[insert delete hardDelete update upsert]
|
20
|
+
|
21
|
+
def initialize(operation:, object:, id: nil, records: nil)
|
22
|
+
@operation = operation.to_sym
|
23
|
+
@object = object
|
24
|
+
@id = id
|
25
|
+
@records = records
|
26
|
+
@state = nil
|
27
|
+
@content_url = nil
|
28
|
+
initialize_state_methods
|
29
|
+
end
|
30
|
+
|
31
|
+
def create
|
32
|
+
request_body = default_job_options.merge(operation: operation, object: object)
|
33
|
+
response = client.post("#{ingest_path}/", request_body)
|
34
|
+
update_attributes_from(response)
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
def upload
|
39
|
+
headers = {"Content-Type": 'text/csv'}
|
40
|
+
response = client.put(content_url, records.to_csv, headers)
|
41
|
+
response
|
42
|
+
end
|
43
|
+
|
44
|
+
def result
|
45
|
+
ActiveForce::Bulk::JobResult.new(job: self)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.run(...)
|
49
|
+
job = new(...)
|
50
|
+
job.create
|
51
|
+
job.upload
|
52
|
+
job.run
|
53
|
+
job
|
54
|
+
end
|
55
|
+
|
56
|
+
def failed_results
|
57
|
+
client.get("#{ingest_path}/#{id}/failedResults/")
|
58
|
+
end
|
59
|
+
|
60
|
+
def successful_results
|
61
|
+
client.get("#{ingest_path}/#{id}/successfulResults/")
|
62
|
+
end
|
63
|
+
|
64
|
+
def info
|
65
|
+
response = client.get("#{ingest_path}/#{id}")
|
66
|
+
update_attributes_from(response)
|
67
|
+
response
|
68
|
+
end
|
69
|
+
|
70
|
+
def run
|
71
|
+
change_state(STATES[:UploadComplete])
|
72
|
+
end
|
73
|
+
|
74
|
+
def abort
|
75
|
+
change_state(STATES[:Aborted])
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete
|
79
|
+
response = client.delete("#{ingest_path}/#{id}")
|
80
|
+
response
|
81
|
+
end
|
82
|
+
|
83
|
+
def finished?
|
84
|
+
job_complete? || failed? || aborted?
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
attr_writer :state, :object, :operation, :content_url
|
89
|
+
|
90
|
+
|
91
|
+
def ingest_path
|
92
|
+
"/services/data/v#{client.options[:api_version]}/jobs/ingest"
|
93
|
+
end
|
94
|
+
|
95
|
+
def client
|
96
|
+
@client ||= ActiveForce.sfdc_client
|
97
|
+
end
|
98
|
+
|
99
|
+
def default_job_options
|
100
|
+
{
|
101
|
+
columnDelimiter: 'COMMA',
|
102
|
+
contentType: 'CSV',
|
103
|
+
lineEnding: 'LF',
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def change_state(value)
|
108
|
+
request_body = {state: value}
|
109
|
+
headers = {"Content-Type": "application/json"}
|
110
|
+
response = client.patch("#{ingest_path}/#{id}", request_body, headers)
|
111
|
+
update_attributes_from(response)
|
112
|
+
response
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_attributes_from(response)
|
116
|
+
return unless response.body.present?
|
117
|
+
|
118
|
+
%i[id state object operation contentUrl].each do |attr|
|
119
|
+
self.send("#{attr.to_s.underscore}=", response.body[attr.to_s]) if response.body[attr.to_s].present?
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Defines question methods for various states of the job, e.g. #open?, #in_progress?, etc.
|
124
|
+
def initialize_state_methods
|
125
|
+
STATES.values.each do |state|
|
126
|
+
state_method = <<-STATE_METHOD
|
127
|
+
def #{state.to_s.underscore}?
|
128
|
+
@state == '#{state}'
|
129
|
+
end
|
130
|
+
STATE_METHOD
|
131
|
+
self.class_eval(state_method)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ActiveForce
|
2
|
+
module Bulk
|
3
|
+
class JobResult
|
4
|
+
attr_reader :job, :failed, :successful, :stats, :errors
|
5
|
+
|
6
|
+
def initialize(job:)
|
7
|
+
@job = job
|
8
|
+
@stats = result_from_job_info
|
9
|
+
@failed = failed_results
|
10
|
+
@successful = successful_results
|
11
|
+
@errors = errors_from_failed_results
|
12
|
+
end
|
13
|
+
|
14
|
+
def success?
|
15
|
+
failed.blank? && successful.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
attr_writer :errors, :failed, :successful
|
20
|
+
|
21
|
+
def errors_from_failed_results
|
22
|
+
return [] if @stats[:number_records_failed].zero? || self.failed.blank?
|
23
|
+
|
24
|
+
self.errors = self.failed.pluck('sf__Error').uniq
|
25
|
+
end
|
26
|
+
|
27
|
+
def failed_results
|
28
|
+
return [] if @stats[:number_records_failed].zero?
|
29
|
+
|
30
|
+
response = job.failed_results
|
31
|
+
self.failed = CSV.parse(response.body, headers: true).map(&:to_h)
|
32
|
+
end
|
33
|
+
|
34
|
+
def successful_results
|
35
|
+
response = job.successful_results
|
36
|
+
self.successful = CSV.parse(response.body, headers: true).map(&:to_h)
|
37
|
+
end
|
38
|
+
|
39
|
+
def job_info
|
40
|
+
job.info
|
41
|
+
end
|
42
|
+
|
43
|
+
def result_from_job_info
|
44
|
+
job_info&.body.slice('numberRecordsProcessed', 'numberRecordsFailed', 'totalProcessingTime').transform_keys { |k| k.to_s.underscore.to_sym }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module ActiveForce
|
4
|
+
module Bulk
|
5
|
+
class Records
|
6
|
+
attr_reader :headers, :data
|
7
|
+
def initialize(headers:, data:)
|
8
|
+
@headers = headers
|
9
|
+
@data = data
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_csv
|
13
|
+
CSV.generate(String.new, headers: headers, write_headers: true) do |csv|
|
14
|
+
data.each { |row| csv << row }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.parse_from_attributes(records)
|
19
|
+
headers = records.first.keys.sort.map(&:to_s)
|
20
|
+
data = records.map { |r| r.sort.pluck(-1) }
|
21
|
+
new(headers: headers, data: data)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'active_force/bulk/job'
|
2
|
+
require 'active_force/bulk/records'
|
3
|
+
|
4
|
+
module ActiveForce
|
5
|
+
module Bulk
|
6
|
+
class TimeoutError < Timeout::Error; end
|
7
|
+
TIMEOUT_MESSAGE = 'Bulk job execution expired based on timeout of %{timeout} seconds'.freeze
|
8
|
+
|
9
|
+
def bulk_insert_all(attributes, options={})
|
10
|
+
run_bulk_job(:insert, attributes, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def bulk_update_all(attributes, options={})
|
14
|
+
run_bulk_job(:update, attributes, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def bulk_delete_all(attributes, options={})
|
18
|
+
run_bulk_job(:delete, attributes, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def default_options
|
24
|
+
{
|
25
|
+
timeout: 30,
|
26
|
+
sleep: 0.02 # short sleep so we can end our poll loop more quickly
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_bulk_job(operation, attributes, options)
|
31
|
+
runtime_options = default_options.merge(options)
|
32
|
+
records = Records.parse_from_attributes(translate_to_sf(attributes))
|
33
|
+
job = Job.run(operation: operation, object: self.table_name, records: records)
|
34
|
+
Timeout.timeout(runtime_options[:timeout], ActiveForce::Bulk::TimeoutError, TIMEOUT_MESSAGE % runtime_options) do
|
35
|
+
until job.finished? do
|
36
|
+
job.info
|
37
|
+
sleep(runtime_options[:sleep])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
job.result
|
41
|
+
end
|
42
|
+
|
43
|
+
def translate_to_sf(attributes)
|
44
|
+
attributes.map{ |r| self.mapping.translate_to_sf(r) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/active_force/query.rb
CHANGED
data/lib/active_force/sobject.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'active_model'
|
2
2
|
require 'active_force/active_query'
|
3
3
|
require 'active_force/association'
|
4
|
+
require 'active_force/bulk'
|
4
5
|
require 'active_force/mapping'
|
5
6
|
require 'yaml'
|
6
7
|
require 'forwardable'
|
@@ -19,6 +20,7 @@ module ActiveForce
|
|
19
20
|
extend ActiveModel::Callbacks
|
20
21
|
include ActiveModel::Serializers::JSON
|
21
22
|
extend ActiveForce::Association
|
23
|
+
extend ActiveForce::Bulk
|
22
24
|
|
23
25
|
|
24
26
|
define_model_callbacks :build, :create, :update, :save, :destroy
|
@@ -129,6 +131,14 @@ module ActiveForce
|
|
129
131
|
new(args).create!
|
130
132
|
end
|
131
133
|
|
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
|
+
|
132
142
|
def save!
|
133
143
|
run_callbacks :save do
|
134
144
|
if persisted?
|
@@ -231,7 +241,7 @@ module ActiveForce
|
|
231
241
|
end
|
232
242
|
|
233
243
|
def default_attributes
|
234
|
-
@attributes.each_value.select do |value|
|
244
|
+
@attributes.each_value.select do |value|
|
235
245
|
value.is_a?(ActiveModel::Attribute::UserProvidedDefault) || value.instance_values["original_attribute"].is_a?(ActiveModel::Attribute::UserProvidedDefault)
|
236
246
|
end.map(&:name)
|
237
247
|
end
|
data/lib/active_force/version.rb
CHANGED
data/lib/active_force.rb
CHANGED
@@ -18,7 +18,6 @@ describe ActiveForce::ActiveQuery do
|
|
18
18
|
]
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
21
|
before do
|
23
22
|
allow(active_query).to receive(:sfdc_client).and_return client
|
24
23
|
allow(active_query).to receive(:build).and_return Object.new
|
@@ -40,6 +39,72 @@ describe ActiveForce::ActiveQuery do
|
|
40
39
|
end
|
41
40
|
end
|
42
41
|
|
42
|
+
describe '#blank? delegation' do
|
43
|
+
before do
|
44
|
+
allow(client).to receive(:query).and_return(api_result)
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when there are no records' do
|
48
|
+
let(:api_result) { [] }
|
49
|
+
|
50
|
+
it 'returns true' do
|
51
|
+
result = active_query.where("Text_Label = 'foo'").blank?
|
52
|
+
expect(result).to be true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when records are returned' do
|
57
|
+
it 'returns false' do
|
58
|
+
result = active_query.where("Text_Label = 'foo'").blank?
|
59
|
+
expect(result).to be false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#present? delegation' do
|
65
|
+
before do
|
66
|
+
allow(client).to receive(:query).and_return(api_result)
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'when there are no records' do
|
70
|
+
let(:api_result) { [] }
|
71
|
+
|
72
|
+
it 'returns false' do
|
73
|
+
result = active_query.where("Text_Label = 'foo'").present?
|
74
|
+
expect(result).to be false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'when there are records' do
|
79
|
+
it 'returns true' do
|
80
|
+
result = active_query.where("Text_Label = 'foo'").present?
|
81
|
+
expect(result).to be true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#any? delegation' do
|
87
|
+
before do
|
88
|
+
allow(client).to receive(:query).and_return(api_result)
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'when there are no records' do
|
92
|
+
let(:api_result) { [] }
|
93
|
+
|
94
|
+
it 'returns true' do
|
95
|
+
result = active_query.where("Text_Label = 'foo'").any?
|
96
|
+
expect(result).to be false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when records are returned' do
|
101
|
+
it 'returns false' do
|
102
|
+
result = active_query.where("Text_Label = 'foo'").any?
|
103
|
+
expect(result).to be true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
43
108
|
describe "select only some field using mappings" do
|
44
109
|
it "should return a query only with selected field" do
|
45
110
|
new_query = active_query.select(:field)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveForce::Bulk::JobResult do
|
4
|
+
subject { described_class.new(job: job)}
|
5
|
+
let(:job) { instance_double(ActiveForce::Bulk::Job)}
|
6
|
+
|
7
|
+
let(:failed_results_response) do
|
8
|
+
Faraday::Response.new(status: 200, response_body: "\"sf__Id\",\"sf__Error\"\n\"45678\",\"Something failed.\"")
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:successful_results_response) do
|
12
|
+
Faraday::Response.new(status: 200, response_body: "\"sf__Id\",\"sf__Error\"\n\"12354\",\"false\"\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:successful_info) do
|
16
|
+
{"id"=>"22222222222222",
|
17
|
+
"operation"=>"update",
|
18
|
+
"object"=>"FooBar__c",
|
19
|
+
"createdById"=>"11111111111111111",
|
20
|
+
"createdDate"=>"2023-09-22T16:39:09.000+0000",
|
21
|
+
"systemModstamp"=>"2023-09-22T16:39:13.000+0000",
|
22
|
+
"state"=>"JobComplete",
|
23
|
+
"concurrencyMode"=>"Parallel",
|
24
|
+
"contentType"=>"CSV",
|
25
|
+
"apiVersion"=>58.0,
|
26
|
+
"jobType"=>"V2Ingest",
|
27
|
+
"lineEnding"=>"LF",
|
28
|
+
"columnDelimiter"=>"COMMA",
|
29
|
+
"numberRecordsProcessed"=>1,
|
30
|
+
"numberRecordsFailed"=>0,
|
31
|
+
"retries"=>0,
|
32
|
+
"totalProcessingTime"=>713,
|
33
|
+
"apiActiveProcessingTime"=>323,
|
34
|
+
"apexProcessingTime"=>3}
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:failed_info) do
|
38
|
+
{"id"=>"33333333333333",
|
39
|
+
"operation"=>"update",
|
40
|
+
"object"=>"FooBar__c",
|
41
|
+
"createdById"=>"11111111111111111",
|
42
|
+
"createdDate"=>"2023-09-22T16:39:09.000+0000",
|
43
|
+
"systemModstamp"=>"2023-09-22T16:39:13.000+0000",
|
44
|
+
"state"=>"JobComplete",
|
45
|
+
"concurrencyMode"=>"Parallel",
|
46
|
+
"contentType"=>"CSV",
|
47
|
+
"apiVersion"=>58.0,
|
48
|
+
"jobType"=>"V2Ingest",
|
49
|
+
"lineEnding"=>"LF",
|
50
|
+
"columnDelimiter"=>"COMMA",
|
51
|
+
"numberRecordsProcessed"=>1,
|
52
|
+
"numberRecordsFailed"=>1,
|
53
|
+
"retries"=>0,
|
54
|
+
"totalProcessingTime"=>713,
|
55
|
+
"apiActiveProcessingTime"=>323,
|
56
|
+
"apexProcessingTime"=>3}
|
57
|
+
end
|
58
|
+
|
59
|
+
let(:successful_job_info_response) do
|
60
|
+
Faraday::Response.new(status: 200, response_body: successful_info)
|
61
|
+
end
|
62
|
+
|
63
|
+
let(:failed_job_info_response) do
|
64
|
+
Faraday::Response.new(status: 200, response_body: failed_info)
|
65
|
+
end
|
66
|
+
|
67
|
+
before do
|
68
|
+
allow(job).to receive(:failed_results).and_return(failed_results_response)
|
69
|
+
allow(job).to receive(:successful_results).and_return(successful_results_response)
|
70
|
+
allow(job).to receive(:info).and_return(successful_job_info_response)
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '::new' do
|
74
|
+
it 'uses the passed in job object to pull job info' do
|
75
|
+
expect(subject.stats[:total_processing_time]).to eq successful_info['totalProcessingTime']
|
76
|
+
expect(subject.stats[:number_records_processed]).to eq successful_info['numberRecordsProcessed']
|
77
|
+
expect(subject.stats[:number_records_failed]).to eq successful_info['numberRecordsFailed']
|
78
|
+
expect(subject.failed).to be_empty
|
79
|
+
expect(subject.successful.size).to eq 1
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#success?' do
|
84
|
+
context 'when no failed results' do
|
85
|
+
it 'returns true' do
|
86
|
+
expect(subject.success?).to be true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
context 'when there are failed results' do
|
90
|
+
before do
|
91
|
+
allow(job).to receive(:info).and_return(failed_job_info_response)
|
92
|
+
end
|
93
|
+
it 'returns false' do
|
94
|
+
expect(subject.success?).to be false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#errors' do
|
100
|
+
context 'when there are failed results' do
|
101
|
+
before do
|
102
|
+
allow(job).to receive(:info).and_return(failed_job_info_response)
|
103
|
+
end
|
104
|
+
it 'returns an array of errors' do
|
105
|
+
expect(subject.errors).to eq ['Something failed.']
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'when there are no failed results' do
|
110
|
+
it 'returns an empty array' do
|
111
|
+
expect(subject.errors).to eq []
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveForce::Bulk::Job do
|
4
|
+
subject { described_class.new(**args)}
|
5
|
+
let(:args) { { operation: operation, object: object, id: id, records: records } }
|
6
|
+
let(:operation) { described_class::OPERATIONS.sample }
|
7
|
+
let(:object) { 'Whizbang__c' }
|
8
|
+
let(:id) { nil }
|
9
|
+
let(:records) { instance_double(ActiveForce::Bulk::Records) }
|
10
|
+
let(:sfdc_client) { spy(ActiveForce.sfdc_client.class)}
|
11
|
+
let(:state) { 'Open' }
|
12
|
+
let(:api_version) { '58.0' }
|
13
|
+
let(:ingest_url) { "/services/data/v#{api_version}/jobs/ingest" }
|
14
|
+
|
15
|
+
before do
|
16
|
+
allow(ActiveForce).to receive(:sfdc_client).and_return(sfdc_client)
|
17
|
+
allow(sfdc_client).to receive(:options).and_return({api_version: api_version})
|
18
|
+
allow(records).to receive(:to_csv)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#finished?' do
|
22
|
+
let(:finished_state) { described_class::STATES.slice(:JobComplete, :Failed, :Aborted).values.sample }
|
23
|
+
let(:unfinished_state) { described_class::STATES.except(:JobComplete, :Failed, :Aborted).values.sample }
|
24
|
+
|
25
|
+
context 'when job is in one of the finished states' do
|
26
|
+
it 'returns true' do
|
27
|
+
job = subject
|
28
|
+
job.instance_variable_set(:@state, finished_state)
|
29
|
+
expect(job.finished?).to be true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when job is NOT in one of the finished states' do
|
34
|
+
it 'returns false' do
|
35
|
+
job = subject
|
36
|
+
job.instance_variable_set(:@state, unfinished_state)
|
37
|
+
expect(job.finished?).to be false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '::run' do
|
43
|
+
let(:job) { spy(described_class)}
|
44
|
+
|
45
|
+
before do
|
46
|
+
allow(described_class).to receive(:new).and_return(job)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'creates, uploads, and runs the job via the SF API' do
|
50
|
+
described_class.run(**args)
|
51
|
+
expect(job).to have_received(:create)
|
52
|
+
expect(job).to have_received(:upload)
|
53
|
+
expect(job).to have_received(:run)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#run' do
|
58
|
+
it 'PATCHs the job state to SF API to run the job' do
|
59
|
+
subject.run
|
60
|
+
expect(sfdc_client).to have_received(:patch).with("#{ingest_url}/#{id}", {state: described_class::STATES[:UploadComplete]}, anything)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#abort' do
|
65
|
+
it 'PATCHs the job state to SF API to abort the job' do
|
66
|
+
subject.abort
|
67
|
+
expect(sfdc_client).to have_received(:patch).with("#{ingest_url}/#{id}", {state: described_class::STATES[:Aborted]}, anything)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#delete' do
|
72
|
+
it 'DELETEs the job from SF API' do
|
73
|
+
subject.delete
|
74
|
+
expect(sfdc_client).to have_received(:delete).with("#{ingest_url}/#{id}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#failed_results' do
|
79
|
+
it 'GETs job info from SF API' do
|
80
|
+
subject.failed_results
|
81
|
+
expect(sfdc_client).to have_received(:get).with("#{ingest_url}/#{id}/failedResults/")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '#successful_results' do
|
86
|
+
it 'GETs successful results from SF API' do
|
87
|
+
subject.successful_results
|
88
|
+
expect(sfdc_client).to have_received(:get).with("#{ingest_url}/#{id}/successfulResults/")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#info' do
|
93
|
+
it 'GETs job info from SF API' do
|
94
|
+
subject.info
|
95
|
+
expect(sfdc_client).to have_received(:get).with("#{ingest_url}/#{id}")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#upload' do
|
100
|
+
it 'PUTs CSV to SF API to upload records to job' do
|
101
|
+
subject.upload
|
102
|
+
expect(sfdc_client).to have_received(:put).with(subject.content_url, anything, hash_including("Content-Type": 'text/csv'))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#create' do
|
107
|
+
it 'POSTs to SF API to create a job' do
|
108
|
+
subject.create
|
109
|
+
expect(sfdc_client).to have_received(:post).with("#{ingest_url}/", hash_including(operation: operation, object: object))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveForce::Bulk::Records do
|
4
|
+
subject { described_class.new(headers: headers, data: data)}
|
5
|
+
let(:headers) { %w[header1 header2] }
|
6
|
+
let(:data) do
|
7
|
+
[
|
8
|
+
%w[value1 value2],
|
9
|
+
%w[value3 value4],
|
10
|
+
]
|
11
|
+
end
|
12
|
+
describe '#to_csv' do
|
13
|
+
it 'returns CSV with headers' do
|
14
|
+
expect(subject.to_csv).to eq "header1,header2\nvalue1,value2\nvalue3,value4\n"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
describe '::parse_from_attributes' do
|
18
|
+
subject { described_class.parse_from_attributes(attributes) }
|
19
|
+
let(:attributes) do
|
20
|
+
[
|
21
|
+
{ header1: 'value1', header2: 'value2'},
|
22
|
+
{ header1: 'value3', header2: 'value4'},
|
23
|
+
]
|
24
|
+
end
|
25
|
+
it 'parses array of hash attributes into Records object with headers and data' do
|
26
|
+
records = subject
|
27
|
+
expect(records).to be_a described_class
|
28
|
+
expect(records.headers).to eq headers
|
29
|
+
expect(records.data).to eq data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Set up a new SObject to test the mixin.
|
4
|
+
class FooBarSObject < ActiveForce::SObject
|
5
|
+
field :id, from: 'Id'
|
6
|
+
field :baz_id, from: 'Baz_id__c'
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ActiveForce::Bulk do
|
10
|
+
subject { FooBarSObject }
|
11
|
+
let(:attributes) do
|
12
|
+
[
|
13
|
+
{id: 1, baz_id: 1 },
|
14
|
+
{id: 2, baz_id: 1 },
|
15
|
+
{id: 3, baz_id: 2 },
|
16
|
+
]
|
17
|
+
end
|
18
|
+
let(:job) { double(ActiveForce::Bulk::Job) }
|
19
|
+
let(:timeout_message) { /Bulk job execution expired based on timeout of/ }
|
20
|
+
before do
|
21
|
+
allow(job).to receive(:result)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '::bulk_insert_all' do
|
25
|
+
before do
|
26
|
+
allow(ActiveForce::Bulk::Job).to receive(:run).with(hash_including(operation: :insert)).and_return(job)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'runs a bulk insert job' do
|
30
|
+
allow(job).to receive(:finished?).and_return(true)
|
31
|
+
subject.bulk_insert_all(attributes)
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when job takes a while to run' do
|
35
|
+
it 'polls job info until job is finished' do
|
36
|
+
allow(job).to receive(:finished?).and_return(false, false, true)
|
37
|
+
allow(job).to receive(:info)
|
38
|
+
subject.bulk_insert_all(attributes)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when job run exceeds timeout' do
|
43
|
+
it 'raises error' do
|
44
|
+
allow(job).to receive(:finished?).and_return(false)
|
45
|
+
allow(job).to receive(:info)
|
46
|
+
expect do
|
47
|
+
subject.bulk_insert_all(attributes, timeout: 0.1)
|
48
|
+
end.to raise_error(ActiveForce::Bulk::TimeoutError, timeout_message)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '::bulk_update_all' do
|
54
|
+
before do
|
55
|
+
allow(ActiveForce::Bulk::Job).to receive(:run).with(hash_including(operation: :update)).and_return(job)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'runs a bulk insert job' do
|
59
|
+
allow(job).to receive(:finished?).and_return(true)
|
60
|
+
subject.bulk_update_all(attributes)
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when job takes a while to run' do
|
64
|
+
it 'polls job info until job is finished' do
|
65
|
+
allow(job).to receive(:finished?).and_return(false, false, true)
|
66
|
+
allow(job).to receive(:info)
|
67
|
+
subject.bulk_update_all(attributes)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when job run exceeds timeout' do
|
72
|
+
it 'raises error' do
|
73
|
+
allow(job).to receive(:finished?).and_return(false)
|
74
|
+
allow(job).to receive(:info)
|
75
|
+
expect do
|
76
|
+
subject.bulk_update_all(attributes, timeout: 0.1)
|
77
|
+
end.to raise_error(ActiveForce::Bulk::TimeoutError, timeout_message)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '::bulk_delete_all' do
|
83
|
+
before do
|
84
|
+
allow(ActiveForce::Bulk::Job).to receive(:run).with(hash_including(operation: :delete)).and_return(job)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'runs a bulk insert job' do
|
88
|
+
allow(job).to receive(:finished?).and_return(true)
|
89
|
+
subject.bulk_delete_all(attributes)
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when job takes a while to run' do
|
93
|
+
it 'polls job info until job is finished' do
|
94
|
+
allow(job).to receive(:finished?).and_return(false, false, true)
|
95
|
+
allow(job).to receive(:info)
|
96
|
+
subject.bulk_delete_all(attributes)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when job run exceeds timeout' do
|
101
|
+
it 'raises error' do
|
102
|
+
allow(job).to receive(:finished?).and_return(false)
|
103
|
+
allow(job).to receive(:info)
|
104
|
+
expect do
|
105
|
+
subject.bulk_delete_all(attributes, timeout: 0.1)
|
106
|
+
end.to raise_error(ActiveForce::Bulk::TimeoutError, timeout_message)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -168,14 +168,30 @@ describe ActiveForce::Query do
|
|
168
168
|
end
|
169
169
|
|
170
170
|
describe '.last' do
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
171
|
+
context 'without any argument' do
|
172
|
+
it 'should return the query for the last record' do
|
173
|
+
expect(query.last.to_s).to eq 'SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT 1'
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should not update the original query" do
|
177
|
+
new_query = query.last
|
178
|
+
expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
|
179
|
+
expect(new_query.to_s).to eq 'SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT 1'
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'with an argument' do
|
184
|
+
let(:last_argument) { 3 }
|
185
|
+
|
186
|
+
it 'should return the query for the last n records' do
|
187
|
+
expect(query.last(last_argument).to_s).to eq "SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT #{last_argument}"
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should not update the original query" do
|
191
|
+
new_query = query.last last_argument
|
192
|
+
expect(query.to_s).to eq "SELECT Id, name, etc FROM table_name"
|
193
|
+
expect(new_query.to_s).to eq "SELECT Id, name, etc FROM table_name ORDER BY Id DESC LIMIT #{last_argument}"
|
194
|
+
end
|
179
195
|
end
|
180
196
|
end
|
181
197
|
|
@@ -225,6 +225,13 @@ module ActiveForce
|
|
225
225
|
end
|
226
226
|
end
|
227
227
|
|
228
|
+
context 'when assocation has a scope' do
|
229
|
+
it 'formulates the correct SOQL query with the scope applied' do
|
230
|
+
soql = Post.includes(:impossible_comments).where(id: '1234').to_s
|
231
|
+
expect(soql).to eq "SELECT Id, Title__c, BlogId, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comments__r WHERE (1 = 0)) FROM Post__c WHERE (Id = '1234')"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
228
235
|
context 'with namespaced SObjects' do
|
229
236
|
it 'formulates the correct SOQL query' do
|
230
237
|
soql = Salesforce::Quota.includes(:prez_clubs).where(id: '123').to_s
|
@@ -286,9 +293,26 @@ module ActiveForce
|
|
286
293
|
end
|
287
294
|
end
|
288
295
|
|
296
|
+
context 'has_one' do
|
297
|
+
context 'when assocation has a scope' do
|
298
|
+
it 'formulates the correct SOQL query with the scope applied' do
|
299
|
+
soql = Post.includes(:last_comment).where(id: '1234').to_s
|
300
|
+
expect(soql).to eq "SELECT Id, Title__c, BlogId, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__r WHERE (NOT ((Body__c = NULL))) ORDER BY CreatedDate DESC) FROM Post__c WHERE (Id = '1234')"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
289
306
|
context 'when invalid associations are passed' do
|
290
|
-
|
291
|
-
|
307
|
+
context 'when the association is not defined' do
|
308
|
+
it 'raises an error' do
|
309
|
+
expect { Quota.includes(:invalid).find('123') }.to raise_error(ActiveForce::Association::InvalidAssociationError, 'Association named invalid was not found on Quota')
|
310
|
+
end
|
311
|
+
end
|
312
|
+
context 'when the association is scoped and accepts an argument' do
|
313
|
+
it 'raises and error' do
|
314
|
+
expect { Post.includes(:reply_comments).find('1234')}.to raise_error(ActiveForce::Association::InvalidEagerLoadAssociation)
|
315
|
+
end
|
292
316
|
end
|
293
317
|
end
|
294
318
|
end
|
@@ -313,6 +313,24 @@ describe ActiveForce::SObject do
|
|
313
313
|
expect(Whizbang.create(text: 'some text')).to be_instance_of(Whizbang)
|
314
314
|
end
|
315
315
|
end
|
316
|
+
|
317
|
+
describe 'self.update' do
|
318
|
+
it 'uses the client to update the correct record' do
|
319
|
+
expect(client).to receive(:update!)
|
320
|
+
.with(Whizbang.table_name, { 'Id' => '12345678', 'Text_Label' => 'my text', 'Updated_From__c' => 'Rails' })
|
321
|
+
.and_return(true)
|
322
|
+
Whizbang.update('12345678', text: 'my text')
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
describe 'self.update!' do
|
327
|
+
it 'uses the client to update the correct record' do
|
328
|
+
expect(client).to receive(:update!)
|
329
|
+
.with(Whizbang.table_name, { 'Id' => '123456789', 'Text_Label' => 'some other text', 'Updated_From__c' => 'Rails' })
|
330
|
+
.and_return(true)
|
331
|
+
Whizbang.update('123456789', text: 'some other text')
|
332
|
+
end
|
333
|
+
end
|
316
334
|
end
|
317
335
|
|
318
336
|
describe '.count' do
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_force
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.19.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eloy Espinaco
|
8
8
|
- Pablo Oldani
|
9
9
|
- Armando Andini
|
10
10
|
- José Piccioni
|
11
|
-
autorequire:
|
11
|
+
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2023-
|
14
|
+
date: 2023-11-15 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activemodel
|
@@ -155,6 +155,10 @@ files:
|
|
155
155
|
- lib/active_force/association/has_many_association.rb
|
156
156
|
- lib/active_force/association/has_one_association.rb
|
157
157
|
- lib/active_force/association/relation_model_builder.rb
|
158
|
+
- lib/active_force/bulk.rb
|
159
|
+
- lib/active_force/bulk/job.rb
|
160
|
+
- lib/active_force/bulk/job_result.rb
|
161
|
+
- lib/active_force/bulk/records.rb
|
158
162
|
- lib/active_force/field.rb
|
159
163
|
- lib/active_force/mapping.rb
|
160
164
|
- lib/active_force/query.rb
|
@@ -170,6 +174,10 @@ files:
|
|
170
174
|
- spec/active_force/active_query_spec.rb
|
171
175
|
- spec/active_force/association/relation_model_builder_spec.rb
|
172
176
|
- spec/active_force/association_spec.rb
|
177
|
+
- spec/active_force/bulk/job_result_spec.rb
|
178
|
+
- spec/active_force/bulk/job_spec.rb
|
179
|
+
- spec/active_force/bulk/record_spec.rb
|
180
|
+
- spec/active_force/bulk_spec.rb
|
173
181
|
- spec/active_force/callbacks_spec.rb
|
174
182
|
- spec/active_force/field_spec.rb
|
175
183
|
- spec/active_force/mapping_spec.rb
|
@@ -193,7 +201,7 @@ metadata:
|
|
193
201
|
bug_tracker_uri: https://github.com/Beyond-Finance/active_force/issues
|
194
202
|
changelog_uri: https://github.com/Beyond-Finance/active_force/blob/main/CHANGELOG.md
|
195
203
|
source_code_uri: https://github.com/Beyond-Finance/active_force
|
196
|
-
post_install_message:
|
204
|
+
post_install_message:
|
197
205
|
rdoc_options: []
|
198
206
|
require_paths:
|
199
207
|
- lib
|
@@ -208,14 +216,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
208
216
|
- !ruby/object:Gem::Version
|
209
217
|
version: '0'
|
210
218
|
requirements: []
|
211
|
-
rubygems_version: 3.
|
212
|
-
signing_key:
|
219
|
+
rubygems_version: 3.3.26
|
220
|
+
signing_key:
|
213
221
|
specification_version: 4
|
214
222
|
summary: Help you implement models persisting on Sales Force within Rails using RESTForce
|
215
223
|
test_files:
|
216
224
|
- spec/active_force/active_query_spec.rb
|
217
225
|
- spec/active_force/association/relation_model_builder_spec.rb
|
218
226
|
- spec/active_force/association_spec.rb
|
227
|
+
- spec/active_force/bulk/job_result_spec.rb
|
228
|
+
- spec/active_force/bulk/job_spec.rb
|
229
|
+
- spec/active_force/bulk/record_spec.rb
|
230
|
+
- spec/active_force/bulk_spec.rb
|
219
231
|
- spec/active_force/callbacks_spec.rb
|
220
232
|
- spec/active_force/field_spec.rb
|
221
233
|
- spec/active_force/mapping_spec.rb
|