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