active_force 0.6.1 → 0.7.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 +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
|
[](https://codeclimate.com/github/ionia-corporation/active_force)
|
4
4
|
[](https://gemnasium.com/ionia-corporation/active_force)
|
5
5
|
[](https://codeclimate.com/github/ionia-corporation/active_force)
|
6
|
+
[](http://inch-ci.org/github/ionia-corporation/active_force)
|
6
7
|
[](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
|