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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +43 -17
  4. data/lib/active_attr/dirty.rb +3 -0
  5. data/lib/active_force/active_query.rb +32 -2
  6. data/lib/active_force/association.rb +14 -10
  7. data/lib/active_force/association/association.rb +26 -11
  8. data/lib/active_force/association/belongs_to_association.rb +1 -4
  9. data/lib/active_force/association/eager_load_projection_builder.rb +60 -0
  10. data/lib/active_force/association/has_many_association.rb +15 -5
  11. data/lib/active_force/association/relation_model_builder.rb +70 -0
  12. data/lib/active_force/attribute.rb +30 -0
  13. data/lib/active_force/mapping.rb +78 -0
  14. data/lib/active_force/query.rb +2 -8
  15. data/lib/active_force/sobject.rb +79 -95
  16. data/lib/active_force/table.rb +6 -2
  17. data/lib/active_force/version.rb +1 -1
  18. data/lib/generators/active_force/model/model_generator.rb +1 -0
  19. data/lib/generators/active_force/model/templates/model.rb.erb +0 -2
  20. data/spec/active_force/active_query_spec.rb +39 -12
  21. data/spec/active_force/association/relation_model_builder_spec.rb +62 -0
  22. data/spec/active_force/association_spec.rb +53 -88
  23. data/spec/active_force/attribute_spec.rb +27 -0
  24. data/spec/active_force/callbacks_spec.rb +1 -23
  25. data/spec/active_force/mapping_spec.rb +18 -0
  26. data/spec/active_force/query_spec.rb +32 -54
  27. data/spec/active_force/sobject/includes_spec.rb +290 -0
  28. data/spec/active_force/sobject/table_name_spec.rb +0 -21
  29. data/spec/active_force/sobject_spec.rb +212 -29
  30. data/spec/active_force/table_spec.rb +0 -3
  31. data/spec/fixtures/sobject/single_sobject_hash.yml +2 -0
  32. data/spec/spec_helper.rb +10 -4
  33. data/spec/support/restforce_factories.rb +9 -0
  34. data/spec/support/sobjects.rb +97 -0
  35. data/spec/support/whizbang.rb +25 -7
  36. metadata +18 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f78cd5f272f46a14d23431270acbfa2d6b8226b3
4
- data.tar.gz: e38b5c4f75717978e8862e974a0f8464b52fc1cb
3
+ metadata.gz: f65b3ee4d09a2cc85b6ff35e563104b90e35ef2e
4
+ data.tar.gz: 865295aff9ee4dfc2962fa13225d41041f3100d1
5
5
  SHA512:
6
- metadata.gz: 3953499253bd6e0adbb2b5f9aaf8dc1a2d33d3b08048284b2f4e517954870ae48f83694fb6e4243b1c3d98c3375f2238ad238f420c523ebea65306f64dafa329
7
- data.tar.gz: 4cf78d28885b7b7562c6f21a1f457680d2006709f3b2f8ea89c5ee4ed9c72b5f7f74843cd7ccdd35a82e661687263cfa284d1df14c5f9eb3a68caf52adb97082
6
+ metadata.gz: 54eac72eb1033454434c97b997810587004df5655134574a445f8826318c69eda74783f2a902bd75cffe4addd88a41b49b93451029284d2002ca4c2928c3c988
7
+ data.tar.gz: 509928a1158a20cb8122efa37d98e893bf762ad1b9cf7329f35cf68ffed269fcd738a4613357bf4ec3830e86cffa6002b9d153baf6689356f555f5836f9b0248
@@ -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
- If you want restforce logging on a rails app:
28
+ ## Setup credentials
31
29
 
32
- ```ruby
33
- #Add this to initializers/restforce.rb
34
- Restforce.log = true if Rails.env.development?
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 # from defaults to "Max_Dossage__c"
48
+ field :max_dossage # defaults to "Max_Dossage__c"
45
49
  field :updated_from
46
50
 
47
51
  ##
48
- # Table name is infered from class name.
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
- ### Relationships
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 option parameters in the declaration.
93
+ # Use optional parameters in the declaration.
90
94
 
91
95
  has_many :medications,
92
- where: "Discontinued__c > #{ Date.today.strftime("%Y-%m-%d") }" \
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
- where: { date: Time.now.in_time_zone.strftime("%Y-%m-%d") }
100
+ scoped_as: ->{ where(date: Time.now.in_time_zone.strftime("%Y-%m-%d") }
98
101
 
99
102
  has_many :labs,
100
- where: "Category__c = 'EMR' AND Date__c <> NULL",
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
+
@@ -17,5 +17,8 @@ module ActiveAttr
17
17
  end
18
18
  end
19
19
 
20
+ def attributes_and_changes
21
+ attributes.select{ |attr, key| changed.include? attr }
22
+ end
20
23
  end
21
24
  end
@@ -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
- "#{mappings[key]} = #{enclose_value value}"
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
- module ClassMethods
8
-
9
- def has_many relation_name, options = {}
10
- HasManyAssociation.new(self, relation_name, options)
11
- end
9
+ def associations
10
+ @associations ||= {}
11
+ end
12
12
 
13
- def belongs_to relation_name, options = {}
14
- BelongsToAssociation.new(self, relation_name, options)
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 self.included mod
20
- mod.extend ClassMethods
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 = parent
9
+ def initialize parent, relation_name, options = {}
10
+ @parent = parent
9
11
  @relation_name = relation_name
10
- @options = options
11
- build
12
+ @options = options
13
+ define_relation_method
12
14
  end
13
15
 
14
16
  def relation_model
15
- @options[:model] || @relation_name.to_s.singularize.camelcase.constantize
17
+ options[:model] || relation_name.to_s.singularize.camelcase.constantize
16
18
  end
17
19
 
18
20
  def foreign_key
19
- @options[:foreign_key] || default_foreign_key
21
+ options[:foreign_key] || default_foreign_key
20
22
  end
21
23
 
22
- private
24
+ def relationship_name
25
+ options[:relationship_name] || relation_model.table_name
26
+ end
23
27
 
24
- def build
25
- define_relation_method
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.custom_table_name? ? model.name : model.table_name
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
- @parent.send :define_method, @relation_name do
14
- association_cache.fetch __method__ do
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
- query.options association.options
17
- association_cache[__method__] = query.where association.foreign_key => self.id
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