Skip to content

Commit

Permalink
Merge branch 'nested_select_statements', remote-tracking branch 'orig…
Browse files Browse the repository at this point in the history
…in' into make_uninitialized_attributes_error_instead_of_nil

merge nested_selects
  • Loading branch information
bfrey08 committed Jan 4, 2024
3 parents 591a1e9 + 4ddfb81 + 36478ae commit 105120c
Show file tree
Hide file tree
Showing 23 changed files with 856 additions and 33 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ spec/reports
test/tmp
test/version_tmp
tmp
.idea
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## Not released

- Change `.first` to not query the API if records have already been retrieved (https://github.com/Beyond-Finance/active_force/pull/77)

## 0.20.1
- Revert "ActiveForce .first performance enhancement (#73)" (https://github.com/Beyond-Finance/active_force/pull/76)

## 0.20.0

- Change `.first` to not query the API if records have already been retrieved (https://github.com/Beyond-Finance/active_force/pull/73)
- Bugfix: Transform NULL values for SF Bulk API, which expects "#N/A" (https://github.com/Beyond-Finance/active_force/pull/74)

## 0.19.0

- Bulk API methods. (https://github.com/Beyond-Finance/active_force/pull/65)

## 0.18.0

- Fix eager loading of scoped associations. (https://github.com/Beyond-Finance/active_force/pull/67)
Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Comment.includes(:post)

It is possible to eager load multi level associations

In order to utilize multi level eager loads, the API version should be set to 58.0 or higher when instantiating a Restforce client
In order to utilize multi level eager loads, the API version should be set to 58.0 or higher when instantiating a Restforce client

```ruby
Restforce.new({api_version: '58.0'})
Expand All @@ -212,8 +212,8 @@ Comment.includes({post: {owner: :account}})
Summing the values of a column:
```ruby
Transaction.where(offer_id: 'ABD832024').sum(:amount)
#=> This will query "SELECT SUM(Amount__c)
# FROM Transaction__c
#=> This will query "SELECT SUM(Amount__c)
# FROM Transaction__c
# WHERE offer_id = 'ABD832024'"
```

Expand Down Expand Up @@ -249,6 +249,22 @@ accounts = Account.where(web_enabled: 1).limit(2)
with data from another API, and will only query the other API once.
```

### Bulk Jobs

For more information about usage and limits of the Salesforce Bulk API see the [docs][4].

Convenience class methods have been added to `ActiveForce::SObject` to make it possible to utilize the Salesforce Bulk API v2.0.
The methods are: `bulk_insert_all`, `bulk_update_all`, & `bulk_delete_all`. They all expect input as an Array of attributes as a Hash:
```ruby
[
{ id: '11111111', attribute1: 'value1', attribute2: 'value2'},
{ id: '22222222', attribute1: 'value3', attribute2: 'value4'},
]
```
The attributes will be mapped back to what's expected on the SF side automatically. The response is a `ActiveForce::Bulk::JobResult` object
which can access `successful` & `failed` results, has some `stats`, and the original `job` object that was used to create and process the
Bulk job.

### Model generator

When using rails, you can generate a model with all the fields you have on your SFDC table by running:
Expand All @@ -268,4 +284,5 @@ When using rails, you can generate a model with all the fields you have on your
[1]: http://www.salesforce.com
[2]: https://github.com/ejholmes/restforce
[3]: https://github.com/bkeepers/dotenv
[4]: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm

1 change: 1 addition & 0 deletions lib/active_force.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'active_force/version'
require 'active_force/sobject'
require 'active_force/query'
require 'active_force/bulk'

module ActiveForce

Expand Down
17 changes: 13 additions & 4 deletions lib/active_force/active_query.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'active_support/all'
require 'active_force/query'
require 'active_force/select_builder'
require 'forwardable'

module ActiveForce
Expand All @@ -18,7 +19,7 @@ def initialize(message = nil, table_name = nil, conditions = nil)
class ActiveQuery < Query
extend Forwardable

attr_reader :sobject, :association_mapping, :belongs_to_association_mapping
attr_reader :sobject, :association_mapping, :belongs_to_association_mapping, :nested_query_fields

def_delegators :sobject, :sfdc_client, :build, :table_name, :mappings
def_delegators :to_a, :blank?, :present?, :any?, :each, :map, :inspect, :pluck, :each_with_object
Expand All @@ -29,6 +30,7 @@ def initialize(sobject, custom_table_name = nil)
@belongs_to_association_mapping = {}
super custom_table_name || table_name
fields sobject.fields
@nested_query_fields = []
end

def to_a
Expand Down Expand Up @@ -56,6 +58,10 @@ def limit limit
limit == 1 ? super.to_a.first : super
end

def first
super.to_a.first
end

def not args=nil, *rest
return self if args.nil?

Expand All @@ -68,8 +74,11 @@ def where args=nil, *rest
end

def select *selected_fields
selected_fields.map! { |field| mappings[field] }
super *selected_fields
fields_collection = ActiveForce::SelectBuilder.new(selected_fields, self).parse
nested_query_fields.concat(fields_collection[:nested_query_fields]) if fields_collection[:nested_query_fields]
return self if fields_collection[:non_nested_query_fields].blank?

super *fields_collection[:non_nested_query_fields]
end

def find!(id)
Expand All @@ -91,7 +100,7 @@ def find_by!(conditions)
end

def includes(*relations)
includes_query = Association::EagerLoadBuilderForNestedIncludes.build(relations, sobject)
includes_query = Association::EagerLoadBuilderForNestedIncludes.build(relations, sobject, nil, nested_query_fields)
fields includes_query[:fields]
association_mapping.merge!(includes_query[:association_mapping])
self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ class InvalidAssociationError < StandardError; end
class EagerLoadBuilderForNestedIncludes

class << self
def build(relations, current_sobject, parent_association_field = nil)
new(relations, current_sobject, parent_association_field).projections
def build(relations, current_sobject, parent_association_field = nil, query_fields = nil)
new(relations, current_sobject, parent_association_field, query_fields).projections
end
end

attr_reader :relations, :current_sobject, :association_mapping, :parent_association_field, :fields
attr_reader :relations, :current_sobject, :association_mapping, :parent_association_field, :fields, :query_fields

def initialize(relations, current_sobject, parent_association_field = nil)
def initialize(relations, current_sobject, parent_association_field = nil, query_fields = nil)
@relations = [relations].flatten
@current_sobject = current_sobject
@association_mapping = {}
@parent_association_field = parent_association_field
@query_fields = query_fields
@fields = []
end

Expand All @@ -37,10 +38,17 @@ def projections
end

def build_includes(association)
fields.concat(EagerLoadProjectionBuilder.build(association, parent_association_field))
fields.concat(EagerLoadProjectionBuilder.build(association, parent_association_field, query_fields_for(association)))
association_mapping[association.sfdc_association_field.downcase] = association.relation_name
end

def query_fields_for(association)
return nil if query_fields.blank?
query_fields_with_association = query_fields.find { |nested_field| nested_field[association.relation_name].present? }
return nil if query_fields_with_association.blank?
query_fields_with_association[association.relation_name].map { |field| association.relation_model.mappings[field] }
end

def build_hash_includes(relation, model = current_sobject, parent_association_field = nil)
relation.each do |key, value|
association = model.associations[key]
Expand All @@ -63,9 +71,10 @@ def build_hash_includes(relation, model = current_sobject, parent_association_fi

def build_relation(association, nested_includes)
sub_query = Query.new(association.sfdc_association_field)
sub_query.fields association.relation_model.fields
selected_fields = query_fields_for(association) || association.relation_model.fields
sub_query.fields selected_fields
association_mapping[association.sfdc_association_field.downcase] = association.relation_name
nested_includes_query = self.class.build(nested_includes, association.relation_model)
nested_includes_query = self.class.build(nested_includes, association.relation_model, nil, query_fields)
sub_query.fields nested_includes_query[:fields]
{ fields: ["(#{sub_query})"], association_mapping: nested_includes_query[:association_mapping] }
end
Expand All @@ -77,7 +86,7 @@ def build_relation_for_belongs_to(association, nested_includes)
else
current_parent_association_field = association.sfdc_association_field
end
self.class.build(nested_includes, association.relation_model, current_parent_association_field)
self.class.build(nested_includes, association.relation_model, current_parent_association_field, query_fields)
end
end
end
Expand Down
29 changes: 17 additions & 12 deletions lib/active_force/association/eager_load_projection_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,35 @@ module Association
class InvalidEagerLoadAssociation < StandardError; end
class EagerLoadProjectionBuilder
class << self
def build(association, parent_association_field = nil)
new(association, parent_association_field).projections
def build(association, parent_association_field = nil, query_fields = nil)
new(association, parent_association_field, query_fields).projections
end
end

attr_reader :association, :parent_association_field
attr_reader :association, :parent_association_field, :query_fields

def initialize(association, parent_association_field = nil)
def initialize(association, parent_association_field = nil, query_fields = nil)
@association = association
@parent_association_field = parent_association_field
@query_fields = query_fields
end

def projections
klass = association.class.name.split('::').last
builder_class = ActiveForce::Association.const_get "#{klass}ProjectionBuilder"
builder_class.new(association, parent_association_field).projections
builder_class.new(association, parent_association_field, query_fields).projections
rescue NameError
raise "Don't know how to build projections for #{klass}"
end
end

class AbstractProjectionBuilder
attr_reader :association, :parent_association_field
attr_reader :association, :parent_association_field, :query_fields

def initialize(association, parent_association_field = nil)
def initialize(association, parent_association_field = nil, query_fields = nil)
@association = association
@parent_association_field = parent_association_field
@query_fields = query_fields
end

def projections
Expand All @@ -51,16 +53,18 @@ class HasManyAssociationProjectionBuilder < AbstractProjectionBuilder
# to be pluralized
def projections
relationship_name = association.sfdc_association_field
query = ActiveQuery.new(association.relation_model, relationship_name)
query.fields association.relation_model.fields
selected_fields = query_fields || association.relation_model.fields
query = ActiveQuery.new(association.relation_model, association.sfdc_association_field).select(*selected_fields)

["(#{apply_association_scope(query).to_s})"]
end
end

class HasOneAssociationProjectionBuilder < AbstractProjectionBuilder
def projections
query = ActiveQuery.new(association.relation_model, association.sfdc_association_field)
query.fields association.relation_model.fields
selected_fields = query_fields || association.relation_model.fields
query = ActiveQuery.new(association.relation_model, association.sfdc_association_field).select(*selected_fields)

["(#{apply_association_scope(query).to_s})"]
end
end
Expand All @@ -72,7 +76,8 @@ def projections
else
association.sfdc_association_field
end
association.relation_model.fields.map do |field|
selected_fields = query_fields || association.relation_model.fields
selected_fields.map do |field|
"#{ association_field }.#{ field }"
end
end
Expand Down
47 changes: 47 additions & 0 deletions lib/active_force/bulk.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'active_force/bulk/job'
require 'active_force/bulk/records'

module ActiveForce
module Bulk
class TimeoutError < Timeout::Error; end
TIMEOUT_MESSAGE = 'Bulk job execution expired based on timeout of %{timeout} seconds'.freeze

def bulk_insert_all(attributes, options={})
run_bulk_job(:insert, attributes, options)
end

def bulk_update_all(attributes, options={})
run_bulk_job(:update, attributes, options)
end

def bulk_delete_all(attributes, options={})
run_bulk_job(:delete, attributes, options)
end

private

def default_options
{
timeout: 30,
sleep: 0.02 # short sleep so we can end our poll loop more quickly
}
end

def run_bulk_job(operation, attributes, options)
runtime_options = default_options.merge(options)
records = Records.parse_from_attributes(translate_to_sf(attributes))
job = Job.run(operation: operation, object: self.table_name, records: records)
Timeout.timeout(runtime_options[:timeout], ActiveForce::Bulk::TimeoutError, TIMEOUT_MESSAGE % runtime_options) do
until job.finished? do
job.info
sleep(runtime_options[:sleep])
end
end
job.result
end

def translate_to_sf(attributes)
attributes.map{ |r| self.mapping.translate_to_sf(r) }
end
end
end
Loading

0 comments on commit 105120c

Please sign in to comment.