active_force 0.6.1 → 0.7.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 +7 -0
- data/README.md +43 -17
- data/lib/active_attr/dirty.rb +3 -0
- data/lib/active_force/active_query.rb +32 -2
- data/lib/active_force/association.rb +14 -10
- data/lib/active_force/association/association.rb +26 -11
- data/lib/active_force/association/belongs_to_association.rb +1 -4
- data/lib/active_force/association/eager_load_projection_builder.rb +60 -0
- data/lib/active_force/association/has_many_association.rb +15 -5
- data/lib/active_force/association/relation_model_builder.rb +70 -0
- data/lib/active_force/attribute.rb +30 -0
- data/lib/active_force/mapping.rb +78 -0
- data/lib/active_force/query.rb +2 -8
- data/lib/active_force/sobject.rb +79 -95
- data/lib/active_force/table.rb +6 -2
- data/lib/active_force/version.rb +1 -1
- data/lib/generators/active_force/model/model_generator.rb +1 -0
- data/lib/generators/active_force/model/templates/model.rb.erb +0 -2
- data/spec/active_force/active_query_spec.rb +39 -12
- data/spec/active_force/association/relation_model_builder_spec.rb +62 -0
- data/spec/active_force/association_spec.rb +53 -88
- data/spec/active_force/attribute_spec.rb +27 -0
- data/spec/active_force/callbacks_spec.rb +1 -23
- data/spec/active_force/mapping_spec.rb +18 -0
- data/spec/active_force/query_spec.rb +32 -54
- data/spec/active_force/sobject/includes_spec.rb +290 -0
- data/spec/active_force/sobject/table_name_spec.rb +0 -21
- data/spec/active_force/sobject_spec.rb +212 -29
- data/spec/active_force/table_spec.rb +0 -3
- data/spec/fixtures/sobject/single_sobject_hash.yml +2 -0
- data/spec/spec_helper.rb +10 -4
- data/spec/support/restforce_factories.rb +9 -0
- data/spec/support/sobjects.rb +97 -0
- data/spec/support/whizbang.rb +25 -7
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f65b3ee4d09a2cc85b6ff35e563104b90e35ef2e
|
4
|
+
data.tar.gz: 865295aff9ee4dfc2962fa13225d41041f3100d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54eac72eb1033454434c97b997810587004df5655134574a445f8826318c69eda74783f2a902bd75cffe4addd88a41b49b93451029284d2002ca4c2928c3c988
|
7
|
+
data.tar.gz: 509928a1158a20cb8122efa37d98e893bf762ad1b9cf7329f35cf68ffed269fcd738a4613357bf4ec3830e86cffa6002b9d153baf6689356f555f5836f9b0248
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
3
|
## Not released
|
4
|
+
* Rails4-style conditional has_many associations ([Dan Olson][])
|
5
|
+
* Add `#includes` query method to eager load has_many association. ([Dan Olson][])
|
6
|
+
* Add `#includes` query method to eager load belongs_to association. ([#65][])
|
4
7
|
* SObject#destroy method.
|
5
8
|
|
6
9
|
## 0.6.1
|
@@ -72,11 +75,15 @@
|
|
72
75
|
[#14]: https://github.com/ionia-corporation/active_force/issues/14
|
73
76
|
[#15]: https://github.com/ionia-corporation/active_force/issues/15
|
74
77
|
[#19]: https://github.com/ionia-corporation/active_force/issues/19
|
78
|
+
[#20]: https://github.com/ionia-corporation/active_force/issues/20
|
75
79
|
[#21]: https://github.com/ionia-corporation/active_force/issues/21
|
76
80
|
[#24]: https://github.com/ionia-corporation/active_force/issues/24
|
77
81
|
[#26]: https://github.com/ionia-corporation/active_force/issues/26
|
78
82
|
[#28]: https://github.com/ionia-corporation/active_force/issues/28
|
83
|
+
[#29]: https://github.com/ionia-corporation/active_force/issues/29
|
79
84
|
[#30]: https://github.com/ionia-corporation/active_force/issues/30
|
85
|
+
[#33]: https://github.com/ionia-corporation/active_force/issues/33
|
86
|
+
[#65]: https://github.com/ionia-corporation/active_force/issues/65
|
80
87
|
[Pablo Oldani]: https://github.com/olvap
|
81
88
|
[Armando Andini]: https://github.com/antico5
|
82
89
|
[José Piccioni]: https://github.com/lmhsjackson
|
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
[![Code Climate](http://img.shields.io/codeclimate/github/ionia-corporation/active_force.svg)](https://codeclimate.com/github/ionia-corporation/active_force)
|
4
4
|
[![Dependency Status](http://img.shields.io/gemnasium/ionia-corporation/active_force.svg)](https://gemnasium.com/ionia-corporation/active_force)
|
5
5
|
[![Test Coverage](https://codeclimate.com/github/ionia-corporation/active_force/badges/coverage.svg)](https://codeclimate.com/github/ionia-corporation/active_force)
|
6
|
+
[![Inline docs](http://inch-ci.org/github/ionia-corporation/active_force.png?branch=master)](http://inch-ci.org/github/ionia-corporation/active_force)
|
6
7
|
[![Chat](http://img.shields.io/badge/chat-gitter-brightgreen.svg)](https://gitter.im/ionia-corporation/active_force)
|
7
8
|
|
8
9
|
# ActiveForce
|
@@ -10,9 +11,6 @@
|
|
10
11
|
A ruby gem to interact with [SalesForce][1] as if it were Active Record. It
|
11
12
|
uses [Restforce][2] to interact with the API, so it is fast and stable.
|
12
13
|
|
13
|
-
[1]: http://www.salesforce.com
|
14
|
-
[2]: https://github.com/ejholmes/restforce
|
15
|
-
|
16
14
|
## Installation
|
17
15
|
|
18
16
|
Add this line to your application's Gemfile:
|
@@ -27,12 +25,18 @@ Or install it yourself as:
|
|
27
25
|
|
28
26
|
$ gem install active_force
|
29
27
|
|
30
|
-
|
28
|
+
## Setup credentials
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
[Restforce][2] is used to interact with the API, so you will need to setup
|
31
|
+
environment variables to set up credentials.
|
32
|
+
|
33
|
+
SALESFORCE_USERNAME = your-email@gmail.com
|
34
|
+
SALESFORCE_PASSWORD = your-sfdc-password
|
35
|
+
SALESFORCE_SECURITY_TOKEN = security-token
|
36
|
+
SALESFORCE_CLIENT_ID = your-client-id
|
37
|
+
SALESFORCE_CLIENT_SECRET = your-client-secret
|
38
|
+
|
39
|
+
You might be interested in [dotenv-rails][3] to set up those in development.
|
36
40
|
|
37
41
|
## Usage
|
38
42
|
|
@@ -41,11 +45,11 @@ class Medication < ActiveForce::SObject
|
|
41
45
|
|
42
46
|
field :name, from: 'Name'
|
43
47
|
|
44
|
-
field :max_dossage #
|
48
|
+
field :max_dossage # defaults to "Max_Dossage__c"
|
45
49
|
field :updated_from
|
46
50
|
|
47
51
|
##
|
48
|
-
# Table name is
|
52
|
+
# Table name is inferred from class name.
|
49
53
|
#
|
50
54
|
# self.table_name = 'Medication__c' # default one.
|
51
55
|
|
@@ -78,7 +82,7 @@ Altenative you can try the generator. (requires setting up the connection)
|
|
78
82
|
|
79
83
|
rails generate active_force_model Medication__c
|
80
84
|
|
81
|
-
###
|
85
|
+
### Associations
|
82
86
|
|
83
87
|
#### Has Many
|
84
88
|
|
@@ -86,19 +90,17 @@ Altenative you can try the generator. (requires setting up the connection)
|
|
86
90
|
class Account < ActiveForce::SObject
|
87
91
|
has_many :pages
|
88
92
|
|
89
|
-
# Use
|
93
|
+
# Use optional parameters in the declaration.
|
90
94
|
|
91
95
|
has_many :medications,
|
92
|
-
|
93
|
-
"OR Discontinued__c = NULL"
|
96
|
+
scoped_as: ->{ where("Discontinued__c > ? OR Discontinued__c = ?", Date.today.strftime("%Y-%m-%d"), nil) }
|
94
97
|
|
95
98
|
has_many :today_log_entries,
|
96
99
|
model: DailyLogEntry,
|
97
|
-
|
100
|
+
scoped_as: ->{ where(date: Time.now.in_time_zone.strftime("%Y-%m-%d") }
|
98
101
|
|
99
102
|
has_many :labs,
|
100
|
-
|
101
|
-
order: 'Date__c DESC'
|
103
|
+
scoped_as: ->{ where("Category__c = 'EMR' AND Date__c <> NULL").order('Date__c DESC') }
|
102
104
|
|
103
105
|
end
|
104
106
|
```
|
@@ -113,6 +115,25 @@ class Page < ActiveForce::SObject
|
|
113
115
|
end
|
114
116
|
```
|
115
117
|
|
118
|
+
### Querying
|
119
|
+
|
120
|
+
You can retrieve SObject from the database using chained conditions to build
|
121
|
+
the query.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
Account.where(web_enable: 1, contact_by: ['web', 'email']).limit(2)
|
125
|
+
#=> this will query "SELECT Id, Name, WebEnable__c
|
126
|
+
# FROM Account
|
127
|
+
# WHERE WebEnable__C = 1 AND ContactBy__c IN ('web','email')
|
128
|
+
# LIMIT 2
|
129
|
+
```
|
130
|
+
|
131
|
+
It is also possible to eager load associations:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
Comment.includes(:post)
|
135
|
+
```
|
136
|
+
|
116
137
|
### Model generator
|
117
138
|
|
118
139
|
When using rails, you can generate a model with all the fields you have on your SFDC table by running:
|
@@ -128,3 +149,8 @@ When using rails, you can generate a model with all the fields you have on your
|
|
128
149
|
5. Create new pull request so we can talk about it.
|
129
150
|
6. Once accepted, please add an entry in the CHANGELOG and rebase your changes
|
130
151
|
to squash typos or corrections.
|
152
|
+
|
153
|
+
[1]: http://www.salesforce.com
|
154
|
+
[2]: https://github.com/ejholmes/restforce
|
155
|
+
[3]: https://github.com/bkeepers/dotenv
|
156
|
+
|
data/lib/active_attr/dirty.rb
CHANGED
@@ -48,6 +48,14 @@ module ActiveForce
|
|
48
48
|
where(conditions).limit 1
|
49
49
|
end
|
50
50
|
|
51
|
+
def includes(*relations)
|
52
|
+
relations.each do |relation|
|
53
|
+
association = sobject.associations[relation]
|
54
|
+
fields Association::EagerLoadProjectionBuilder.build association
|
55
|
+
end
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
51
59
|
private
|
52
60
|
|
53
61
|
def build_condition(args, other=[])
|
@@ -98,14 +106,31 @@ module ActiveForce
|
|
98
106
|
|
99
107
|
def build_conditions_from_hash(hash)
|
100
108
|
hash.map do |key, value|
|
101
|
-
|
109
|
+
applicable_predicate mappings[key], value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def applicable_predicate(attribute, value)
|
114
|
+
if value.is_a? Array
|
115
|
+
in_predicate attribute, value
|
116
|
+
else
|
117
|
+
eq_predicate attribute, value
|
102
118
|
end
|
103
119
|
end
|
104
120
|
|
121
|
+
def in_predicate(attribute, values)
|
122
|
+
escaped_values = values.map &method(:enclose_value)
|
123
|
+
"#{attribute} IN (#{escaped_values.join(',')})"
|
124
|
+
end
|
125
|
+
|
126
|
+
def eq_predicate(attribute, value)
|
127
|
+
"#{attribute} = #{enclose_value value}"
|
128
|
+
end
|
129
|
+
|
105
130
|
def enclose_value value
|
106
131
|
case value
|
107
132
|
when String
|
108
|
-
"'#{value}'"
|
133
|
+
"'#{quote_string(value)}'"
|
109
134
|
when NilClass
|
110
135
|
'NULL'
|
111
136
|
else
|
@@ -113,6 +138,11 @@ module ActiveForce
|
|
113
138
|
end
|
114
139
|
end
|
115
140
|
|
141
|
+
def quote_string(s)
|
142
|
+
# From activerecord/lib/active_record/connection_adapters/abstract/quoting.rb, version 4.1.5, line 82
|
143
|
+
s.gsub(/\\/, '\&\&').gsub(/'/, "''")
|
144
|
+
end
|
145
|
+
|
116
146
|
def result
|
117
147
|
sfdc_client.query(self.to_s)
|
118
148
|
end
|
@@ -1,24 +1,28 @@
|
|
1
1
|
require 'active_force/association/association'
|
2
|
+
require 'active_force/association/eager_load_projection_builder'
|
3
|
+
require 'active_force/association/relation_model_builder'
|
2
4
|
require 'active_force/association/has_many_association'
|
3
5
|
require 'active_force/association/belongs_to_association'
|
4
6
|
|
5
7
|
module ActiveForce
|
6
8
|
module Association
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
HasManyAssociation.new(self, relation_name, options)
|
11
|
-
end
|
9
|
+
def associations
|
10
|
+
@associations ||= {}
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
# i.e name = 'Quota__r'
|
14
|
+
def find_association name
|
15
|
+
associations.values.detect do |association|
|
16
|
+
association.represents_sfdc_table? name
|
15
17
|
end
|
16
|
-
|
17
18
|
end
|
18
19
|
|
19
|
-
def
|
20
|
-
|
20
|
+
def has_many relation_name, options = {}
|
21
|
+
associations[relation_name] = HasManyAssociation.new(self, relation_name, options)
|
21
22
|
end
|
22
23
|
|
24
|
+
def belongs_to relation_name, options = {}
|
25
|
+
associations[relation_name] = BelongsToAssociation.new(self, relation_name, options)
|
26
|
+
end
|
23
27
|
end
|
24
28
|
end
|
@@ -1,32 +1,47 @@
|
|
1
1
|
module ActiveForce
|
2
2
|
module Association
|
3
3
|
class Association
|
4
|
+
extend Forwardable
|
5
|
+
def_delegators :relation_model, :build
|
4
6
|
|
5
|
-
attr_accessor :options
|
7
|
+
attr_accessor :options, :relation_name
|
6
8
|
|
7
|
-
def initialize parent, relation_name, options
|
8
|
-
@parent
|
9
|
+
def initialize parent, relation_name, options = {}
|
10
|
+
@parent = parent
|
9
11
|
@relation_name = relation_name
|
10
|
-
@options
|
11
|
-
|
12
|
+
@options = options
|
13
|
+
define_relation_method
|
12
14
|
end
|
13
15
|
|
14
16
|
def relation_model
|
15
|
-
|
17
|
+
options[:model] || relation_name.to_s.singularize.camelcase.constantize
|
16
18
|
end
|
17
19
|
|
18
20
|
def foreign_key
|
19
|
-
|
21
|
+
options[:foreign_key] || default_foreign_key
|
20
22
|
end
|
21
23
|
|
22
|
-
|
24
|
+
def relationship_name
|
25
|
+
options[:relationship_name] || relation_model.table_name
|
26
|
+
end
|
23
27
|
|
24
|
-
|
25
|
-
|
28
|
+
###
|
29
|
+
# Does this association's relation_model represent
|
30
|
+
# +sfdc_table_name+? Examples of +sfdc_table_name+
|
31
|
+
# could be 'Quota__r' or 'Account'.
|
32
|
+
def represents_sfdc_table?(sfdc_table_name)
|
33
|
+
name = sfdc_table_name.sub(/__r\z/, '').singularize
|
34
|
+
relationship_name.sub(/__c\z|__r\z/, '') == name
|
26
35
|
end
|
27
36
|
|
37
|
+
def sfdc_association_field
|
38
|
+
relationship_name.gsub /__c\z/, '__r'
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
28
43
|
def infer_foreign_key_from_model(model)
|
29
|
-
name = model.
|
44
|
+
name = model.custom_table? ? model.name : model.table_name
|
30
45
|
"#{name.downcase}_id".to_sym
|
31
46
|
end
|
32
47
|
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
module ActiveForce
|
2
2
|
module Association
|
3
|
-
|
4
3
|
class BelongsToAssociation < Association
|
5
|
-
|
6
4
|
private
|
7
5
|
|
8
6
|
def default_foreign_key
|
@@ -19,11 +17,10 @@ module ActiveForce
|
|
19
17
|
end
|
20
18
|
|
21
19
|
@parent.send :define_method, "#{_method}=" do |other|
|
22
|
-
send "#{ association.foreign_key }=", other.id
|
20
|
+
send "#{ association.foreign_key }=", other.nil? ? nil : other.id
|
23
21
|
association_cache[_method] = other
|
24
22
|
end
|
25
23
|
end
|
26
24
|
end
|
27
|
-
|
28
25
|
end
|
29
26
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActiveForce
|
2
|
+
module Association
|
3
|
+
class EagerLoadProjectionBuilder
|
4
|
+
class << self
|
5
|
+
def build(association)
|
6
|
+
new(association).projections
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :association
|
11
|
+
|
12
|
+
def initialize(association)
|
13
|
+
@association = association
|
14
|
+
end
|
15
|
+
|
16
|
+
def projections
|
17
|
+
klass = association.class.name.split('::').last
|
18
|
+
builder_class = ActiveForce::Association.const_get "#{klass}ProjectionBuilder"
|
19
|
+
builder_class.new(association).projections
|
20
|
+
rescue NameError
|
21
|
+
raise "Don't know how to build projections for #{klass}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class AbstractProjectionBuilder
|
26
|
+
attr_reader :association
|
27
|
+
|
28
|
+
def initialize(association)
|
29
|
+
@association = association
|
30
|
+
end
|
31
|
+
|
32
|
+
def projections
|
33
|
+
raise "Must define #{self.class.name}#projections"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class HasManyAssociationProjectionBuilder < AbstractProjectionBuilder
|
38
|
+
###
|
39
|
+
# Use ActiveForce::Query to build a subquery for the SFDC
|
40
|
+
# relationship name. Per SFDC convention, the name needs
|
41
|
+
# to be pluralized
|
42
|
+
def projections
|
43
|
+
match = association.sfdc_association_field.match /__r\z/
|
44
|
+
# pluralize the table name, and append '__r' if it was there to begin with
|
45
|
+
relationship_name = association.sfdc_association_field.sub(match.to_s, '').pluralize + match.to_s
|
46
|
+
query = Query.new relationship_name
|
47
|
+
query.fields association.relation_model.fields
|
48
|
+
["(#{query.to_s})"]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class BelongsToAssociationProjectionBuilder < AbstractProjectionBuilder
|
53
|
+
def projections
|
54
|
+
association.relation_model.fields.map do |field|
|
55
|
+
"#{ association.sfdc_association_field }.#{ field }"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module ActiveForce
|
2
2
|
module Association
|
3
3
|
class HasManyAssociation < Association
|
4
|
-
|
5
4
|
private
|
6
5
|
|
7
6
|
def default_foreign_key
|
@@ -10,13 +9,24 @@ module ActiveForce
|
|
10
9
|
|
11
10
|
def define_relation_method
|
12
11
|
association = self
|
13
|
-
|
14
|
-
|
12
|
+
_method = @relation_name
|
13
|
+
@parent.send :define_method, _method do
|
14
|
+
association_cache.fetch _method do
|
15
15
|
query = association.relation_model.query
|
16
|
-
|
17
|
-
|
16
|
+
if scope = association.options[:scoped_as]
|
17
|
+
if scope.arity > 0
|
18
|
+
query.instance_exec self, &scope
|
19
|
+
else
|
20
|
+
query.instance_exec &scope
|
21
|
+
end
|
22
|
+
end
|
23
|
+
association_cache[_method] = query.where association.foreign_key => self.id
|
18
24
|
end
|
19
25
|
end
|
26
|
+
|
27
|
+
@parent.send :define_method, "#{_method}=" do |associated|
|
28
|
+
association_cache[_method] = associated
|
29
|
+
end
|
20
30
|
end
|
21
31
|
end
|
22
32
|
end
|