openstax_active_force 1.0.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.mailmap +3 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +3 -0
  6. data/CHANGELOG.md +98 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +174 -0
  10. data/Rakefile +6 -0
  11. data/active_force.gemspec +30 -0
  12. data/lib/active_attr/dirty.rb +33 -0
  13. data/lib/active_force/active_query.rb +155 -0
  14. data/lib/active_force/association/association.rb +50 -0
  15. data/lib/active_force/association/belongs_to_association.rb +26 -0
  16. data/lib/active_force/association/eager_load_projection_builder.rb +60 -0
  17. data/lib/active_force/association/has_many_association.rb +33 -0
  18. data/lib/active_force/association/relation_model_builder.rb +70 -0
  19. data/lib/active_force/association.rb +28 -0
  20. data/lib/active_force/attribute.rb +30 -0
  21. data/lib/active_force/mapping.rb +78 -0
  22. data/lib/active_force/query.rb +110 -0
  23. data/lib/active_force/sobject.rb +210 -0
  24. data/lib/active_force/standard_types.rb +357 -0
  25. data/lib/active_force/table.rb +37 -0
  26. data/lib/active_force/version.rb +3 -0
  27. data/lib/active_force.rb +13 -0
  28. data/lib/generators/active_force/model/USAGE +8 -0
  29. data/lib/generators/active_force/model/model_generator.rb +62 -0
  30. data/lib/generators/active_force/model/templates/model.rb.erb +5 -0
  31. data/spec/active_force/active_query_spec.rb +178 -0
  32. data/spec/active_force/association/relation_model_builder_spec.rb +62 -0
  33. data/spec/active_force/association_spec.rb +157 -0
  34. data/spec/active_force/attribute_spec.rb +27 -0
  35. data/spec/active_force/callbacks_spec.rb +20 -0
  36. data/spec/active_force/mapping_spec.rb +18 -0
  37. data/spec/active_force/query_spec.rb +126 -0
  38. data/spec/active_force/sobject/includes_spec.rb +290 -0
  39. data/spec/active_force/sobject/table_name_spec.rb +27 -0
  40. data/spec/active_force/sobject_spec.rb +398 -0
  41. data/spec/active_force/table_spec.rb +25 -0
  42. data/spec/active_force_spec.rb +7 -0
  43. data/spec/fixtures/sobject/single_sobject_hash.yml +26 -0
  44. data/spec/spec_helper.rb +16 -0
  45. data/spec/support/fixture_helpers.rb +45 -0
  46. data/spec/support/restforce_factories.rb +9 -0
  47. data/spec/support/sobjects.rb +97 -0
  48. data/spec/support/whizbang.rb +30 -0
  49. metadata +196 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c62816549592114aa2162d2a9de73c80fe873d947fefe47da1d41e2dfcd4a8ea
4
+ data.tar.gz: 4054c4a6ec4d7a29635ec83c7b1b52e360d6ad107e6a1852227bf560002f789d
5
+ SHA512:
6
+ metadata.gz: cf067372afe347c1548d39dbfbf62f7f9788e7c8085239e98d9ed85b95ff3b0ec582f46bfb081cf17a79ebb1046ed0963b7074a1aa00513ebe20ef38444fa776
7
+ data.tar.gz: 64b71c9c2218426c1bde27eb04721482a1aeb7233e10d9e0deca9db5c3424f02acf615ecd5d548f5ee08d2ab3a943be74476ac209d8c460042ad8eb301c84541
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.mailmap ADDED
@@ -0,0 +1,3 @@
1
+ Armando Andini <armando.andini@hotmail.com>
2
+ Jose Piccioni <josepiccioni@gmail.com> <jackson@jackPc.(none)>
3
+ Pablo Oldani <oldani.pablo@gmail.com>
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.6.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ ## Not released
4
+
5
+ ## 0.7.1
6
+
7
+ * Allow sfdc_client to be set. ([#92][])
8
+
9
+ ## 0.7.0
10
+
11
+ * Rails4-style conditional has_many associations ([Dan Olson][])
12
+ * Add `#includes` query method to eager load has_many association. ([Dan Olson][])
13
+ * Add `#includes` query method to eager load belongs_to association. ([#65][])
14
+ * SObject#destroy method.
15
+
16
+ ## 0.6.1
17
+
18
+ * Fix missing require of 'restforce'. Now clients don't need to add an initializer.
19
+
20
+ ## 0.6.0
21
+
22
+ * Add select statement functionality. ([Pablo Oldani][], [#33][])
23
+ * Add callback functionality ([Pablo Oldani][], [#20][])
24
+ * Support bind parameters. ([Dan Olson][], [#29][])
25
+ * Fix when passing nil value in a :where condition. ([Armando Andini][])
26
+ * Model generator complete ([Armando Andini][], [#19][])
27
+
28
+ ## 0.5.0
29
+
30
+ * Provide a default id field for all SObject subclassees ([Dan Olson][], [#30][])
31
+ * Fix Ruby 2.0 compatibility issue ([Dan Olson][], [Pablo Oldani][], [#28][])
32
+ * Normalize rspec syntax to remove deprecation warnings ([Dan Olson][], [#26][])
33
+ * Remove namespace when inferring default SObject.table_name ([Dan Olson][], [#24][])
34
+ * Add create! and save! methods. ([Pablo Oldani][], [#21][])
35
+ * Refactor update and create methods. ([Pablo Oldani][], [#21][])
36
+ * Add a generator. ([José Piccioni][], [#19][])
37
+ * ActiveQuery now provides :each, :map and :inspect. ([Armando Andini][])
38
+ * Add SObject.create class mehtod. ([Pablo Oldani][], [#10][])
39
+ * SObject.field default mapping value follows SFDC API naming convention.
40
+ ([Dan Olson][], [#14][] [#15][])
41
+
42
+ ## 0.4.2
43
+
44
+ * Use ActiveQuery instead of Query. ([Armando Andini][])
45
+ * Add instructions to use validations ([José Piccioni][])
46
+ * Lots of refactoring.
47
+
48
+ ## 0.3.2
49
+
50
+ * Fixed gemspec.
51
+
52
+ ## 0.3.1
53
+
54
+ * Create different classes for associations. ([#4][])
55
+ * Big refactor on has_many association. ([Armando Andini][])
56
+ * Add a lot of specs and refactors. ([Armando Andini][])
57
+ * Add a Finders module. ([Armando Andini][])
58
+ * Add fist and last method to SObject.
59
+
60
+ ## 0.2.0
61
+
62
+ * Add belogns_to and has_many associations.
63
+ * Changed when the SOQL query is sent to the client.
64
+ * Add join method to query to use associtations.
65
+
66
+ ## 0.1.0
67
+
68
+ * Add query builder object to chain conditions.
69
+ * Update update and create methods.
70
+ * Add Campaing standard table name.
71
+
72
+ ## 0.0.6.alfa
73
+
74
+ * ActiveForce::SObject#table_name is auto populated using the class
75
+ name. It adds "__c" to all non standard types.
76
+
77
+ <!--- The following link definition list is generated by PimpMyChangelog --->
78
+
79
+ [#4]: https://github.com/ionia-corporation/active_force/issues/4
80
+ [#9]: https://github.com/ionia-corporation/active_force/issues/9
81
+ [#10]: https://github.com/ionia-corporation/active_force/issues/10
82
+ [#14]: https://github.com/ionia-corporation/active_force/issues/14
83
+ [#15]: https://github.com/ionia-corporation/active_force/issues/15
84
+ [#19]: https://github.com/ionia-corporation/active_force/issues/19
85
+ [#20]: https://github.com/ionia-corporation/active_force/issues/20
86
+ [#21]: https://github.com/ionia-corporation/active_force/issues/21
87
+ [#24]: https://github.com/ionia-corporation/active_force/issues/24
88
+ [#26]: https://github.com/ionia-corporation/active_force/issues/26
89
+ [#28]: https://github.com/ionia-corporation/active_force/issues/28
90
+ [#29]: https://github.com/ionia-corporation/active_force/issues/29
91
+ [#30]: https://github.com/ionia-corporation/active_force/issues/30
92
+ [#33]: https://github.com/ionia-corporation/active_force/issues/33
93
+ [#65]: https://github.com/ionia-corporation/active_force/issues/65
94
+ [#92]: https://github.com/ionia-corporation/active_force/issues/92
95
+ [Pablo Oldani]: https://github.com/olvap
96
+ [Armando Andini]: https://github.com/antico5
97
+ [José Piccioni]: https://github.com/lmhsjackson
98
+ [Dan Olson]: https://github.com/DanOlson
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_force.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Eloy Espinaco
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ [![Gem Version](http://img.shields.io/gem/v/active_force.svg)](http://badge.fury.io/rb/active_force)
2
+ [![Build Status](http://img.shields.io/travis/ionia-corporation/active_force.svg)](https://travis-ci.org/ionia-corporation/active_force)
3
+ [![Test Coverage](https://codeclimate.com/github/ionia-corporation/active_force/badges/coverage.svg)](https://codeclimate.com/github/ionia-corporation/active_force)
4
+ [![Inline docs](http://inch-ci.org/github/ionia-corporation/active_force.png?branch=master)](http://inch-ci.org/github/ionia-corporation/active_force)
5
+ [![Chat](http://img.shields.io/badge/chat-gitter-brightgreen.svg)](https://gitter.im/ionia-corporation/active_force)
6
+
7
+ # ActiveForce
8
+
9
+ A ruby gem to interact with [SalesForce][1] as if it were Active Record. It
10
+ uses [Restforce][2] to interact with the API, so it is fast and stable.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'active_force'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install active_force
25
+
26
+ ## Setup credentials
27
+
28
+ [Restforce][2] is used to interact with the API, so you will need to setup
29
+ environment variables to set up credentials.
30
+
31
+ SALESFORCE_USERNAME = your-email@gmail.com
32
+ SALESFORCE_PASSWORD = your-sfdc-password
33
+ SALESFORCE_SECURITY_TOKEN = security-token
34
+ SALESFORCE_CLIENT_ID = your-client-id
35
+ SALESFORCE_CLIENT_SECRET = your-client-secret
36
+
37
+ You might be interested in [dotenv-rails][3] to set up those in development.
38
+
39
+ Also, you may specify which client to use as a configuration option, which is useful
40
+ when having to reauthenticate utilizing oauth.
41
+
42
+ ```ruby
43
+ ActiveForce.sfdc_client = Restforce.new(
44
+ oauth_token: current_user.oauth_token,
45
+ refresh_token: current_user.refresh_token,
46
+ instance_url: current_user.instance_url,
47
+ client_id: SALESFORCE_CLIENT_ID,
48
+ client_secret: SALESFORCE_CLIENT_SECRET
49
+ )
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ```ruby
55
+ class Medication < ActiveForce::SObject
56
+
57
+ field :name, from: 'Name'
58
+
59
+ field :max_dossage # defaults to "Max_Dossage__c"
60
+ field :updated_from
61
+
62
+ ##
63
+ # You can cast field value using `as`
64
+ # field :address_primary_active, from: 's360a__AddressPrimaryActive__c', as: :boolean
65
+ #
66
+ # Available options are :boolean, :int, :double, :percent, :date, :datetime, :string, :base64,
67
+ # :byte, :ID, :reference, :currency, :textarea, :phone, :url, :email, :combobox, :picklist,
68
+ # :multipicklist, :anyType, :location, :compound
69
+
70
+ ##
71
+ # Table name is inferred from class name.
72
+ #
73
+ # self.table_name = 'Medication__c' # default one.
74
+
75
+ ##
76
+ # Validations
77
+ #
78
+ validates :name, :login, :email, presence: true
79
+
80
+ # Use any validation from active record.
81
+ # validates :text, length: { minimum: 2 }
82
+ # validates :text, format: { with: /\A[a-zA-Z]+\z/, message: "only allows letters" }
83
+ # validates :size, inclusion: { in: %w(small medium large),
84
+ # message: "%{value} is not a valid size" }
85
+
86
+ ##
87
+ # Callbacks
88
+ #
89
+ before_save :set_as_updated_from_rails
90
+
91
+ private
92
+
93
+ def set_as_updated_from_rails
94
+ self.updated_from = 'Rails'
95
+ end
96
+
97
+ end
98
+ ```
99
+
100
+ Altenative you can try the generator. (requires setting up the connection)
101
+
102
+ rails generate active_force_model Medication__c
103
+
104
+ ### Associations
105
+
106
+ #### Has Many
107
+
108
+ ```ruby
109
+ class Account < ActiveForce::SObject
110
+ has_many :pages
111
+
112
+ # Use optional parameters in the declaration.
113
+
114
+ has_many :medications,
115
+ scoped_as: ->{ where("Discontinued__c > ? OR Discontinued__c = ?", Date.today.strftime("%Y-%m-%d"), nil) }
116
+
117
+ has_many :today_log_entries,
118
+ model: DailyLogEntry,
119
+ scoped_as: ->{ where(date: Time.now.in_time_zone.strftime("%Y-%m-%d")) }
120
+
121
+ has_many :labs,
122
+ scoped_as: ->{ where("Category__c = 'EMR' AND Date__c <> NULL").order('Date__c DESC') }
123
+
124
+ end
125
+ ```
126
+
127
+ #### Belongs to
128
+
129
+ ```ruby
130
+ class Page < ActiveForce::SObject
131
+ field :account_id, from: 'Account__c'
132
+
133
+ belongs_to :account
134
+ end
135
+ ```
136
+
137
+ ### Querying
138
+
139
+ You can retrieve SObject from the database using chained conditions to build
140
+ the query.
141
+
142
+ ```ruby
143
+ Account.where(web_enable: 1, contact_by: ['web', 'email']).limit(2)
144
+ #=> this will query "SELECT Id, Name, WebEnable__c
145
+ # FROM Account
146
+ # WHERE WebEnable__C = 1 AND ContactBy__c IN ('web','email')
147
+ # LIMIT 2
148
+ ```
149
+
150
+ It is also possible to eager load associations:
151
+
152
+ ```ruby
153
+ Comment.includes(:post)
154
+ ```
155
+
156
+ ### Model generator
157
+
158
+ When using rails, you can generate a model with all the fields you have on your SFDC table by running:
159
+
160
+ rails g active_force:model <table name>
161
+
162
+ ## Contributing
163
+
164
+ 1. Fork it
165
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
166
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
167
+ 4. Push to the branch (`git push origin my-new-feature`)
168
+ 5. Create new pull request so we can talk about it.
169
+ 6. Once accepted, please add an entry in the CHANGELOG and rebase your changes
170
+ to squash typos or corrections.
171
+
172
+ [1]: https://www.salesforce.com
173
+ [2]: https://github.com/ejholmes/restforce
174
+ [3]: https://github.com/bkeepers/dotenv
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'active_force/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "openstax_active_force"
8
+ spec.version = ActiveForce::VERSION
9
+ spec.authors = ["Eloy Espinaco", "Pablo Oldani", "Armando Andini", "José Piccioni", "JP Slavinsky", "Dante Soares"]
10
+ spec.email = "dante.m.soares@rice.edu"
11
+ spec.description = %q{Use SalesForce as an ActiveModel}
12
+ spec.summary = %q{Help you implement models persisting on Sales Force within Rails using RESTForce}
13
+ spec.homepage = "https://github.com/openstax/active_force"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '>= 1.9.3'
22
+
23
+ spec.add_dependency 'rails', '< 6.0'
24
+ spec.add_dependency 'active_attr'
25
+ spec.add_dependency 'restforce'
26
+
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'pry'
30
+ end
@@ -0,0 +1,33 @@
1
+ require 'active_support'
2
+ require 'active_model/dirty'
3
+ require "active_model/attribute_mutation_tracker"
4
+ require 'active_attr'
5
+
6
+ module ActiveAttr
7
+ module Dirty
8
+ extend ActiveSupport::Concern
9
+ include ActiveModel::Dirty
10
+
11
+ module ClassMethods
12
+ def attribute!(name, options={})
13
+ super(name, options)
14
+ define_method("#{name}=") do |value|
15
+ send("#{name}_will_change!") unless value == read_attribute(name)
16
+ super(value)
17
+ end
18
+ end
19
+ end
20
+
21
+ def attributes_and_changes
22
+ attributes.select { |attr, key| changed.include? attr }
23
+ end
24
+
25
+ def mutations_from_database
26
+ @mutations_from_database ||= ActiveModel::NullMutationTracker.instance
27
+ end
28
+
29
+ def forget_attribute_assignments
30
+ @attributes if defined?(@attributes)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,155 @@
1
+ require 'active_force/query'
2
+ require 'forwardable'
3
+
4
+ module ActiveForce
5
+ class PreparedStatementInvalid < ArgumentError; end
6
+ class ActiveQuery < Query
7
+ extend Forwardable
8
+
9
+ attr_reader :sobject
10
+
11
+ def_delegators :sobject, :sfdc_client, :build, :table_name, :mappings
12
+ def_delegators :to_a, :each, :map, :inspect
13
+
14
+ def initialize sobject
15
+ @sobject = sobject
16
+ super table_name
17
+ fields sobject.fields
18
+ end
19
+
20
+ def to_a
21
+ @records ||= result.to_a.map { |mash| build mash }
22
+ end
23
+
24
+ alias_method :all, :to_a
25
+
26
+ def count
27
+ super
28
+ sfdc_client.query(to_s).first.expr0
29
+ end
30
+
31
+ def limit limit
32
+ super
33
+ limit == 1 ? to_a.first : self
34
+ end
35
+
36
+ def where args=nil, *rest
37
+ return self if args.nil?
38
+ super build_condition args, rest
39
+ self
40
+ end
41
+
42
+ def select *fields
43
+ fields.map! { |field| mappings[field] }
44
+ super *fields
45
+ end
46
+
47
+ def find_by conditions
48
+ where(conditions).limit 1
49
+ end
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
+
59
+ def none
60
+ @records = []
61
+ where(id: '1'*18).where(id: '0'*18)
62
+ end
63
+
64
+ private
65
+
66
+ def build_condition(args, other=[])
67
+ case args
68
+ when String, Array
69
+ build_condition_from_array other.empty? ? args : ([args] + other)
70
+ when Hash
71
+ build_conditions_from_hash args
72
+ else
73
+ args
74
+ end
75
+ end
76
+
77
+ def build_condition_from_array(ary)
78
+ statement, *bind_parameters = ary
79
+ return statement if bind_parameters.empty?
80
+ if bind_parameters.first.is_a? Hash
81
+ replace_named_bind_parameters statement, bind_parameters.first
82
+ else
83
+ replace_bind_parameters statement, bind_parameters
84
+ end
85
+ end
86
+
87
+ def replace_named_bind_parameters(statement, bind_parameters)
88
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do
89
+ key = $2.to_sym
90
+ if bind_parameters.has_key? key
91
+ enclose_value bind_parameters[key]
92
+ else
93
+ raise PreparedStatementInvalid, "missing value for :#{key} in #{statement}"
94
+ end
95
+ end
96
+ end
97
+
98
+ def replace_bind_parameters(statement, values)
99
+ raise_if_bind_arity_mismatch statement.count('?'), values.size
100
+ bound = values.dup
101
+ statement.gsub('?') do
102
+ enclose_value bound.shift
103
+ end
104
+ end
105
+
106
+ def raise_if_bind_arity_mismatch(expected_var_count, actual_var_count)
107
+ if expected_var_count != actual_var_count
108
+ raise PreparedStatementInvalid, "wrong number of bind variables (#{actual_var_count} for #{expected_var_count})"
109
+ end
110
+ end
111
+
112
+ def build_conditions_from_hash(hash)
113
+ hash.map do |key, value|
114
+ applicable_predicate mappings[key], value
115
+ end
116
+ end
117
+
118
+ def applicable_predicate(attribute, value)
119
+ if value.is_a? Array
120
+ in_predicate attribute, value
121
+ else
122
+ eq_predicate attribute, value
123
+ end
124
+ end
125
+
126
+ def in_predicate(attribute, values)
127
+ escaped_values = values.map &method(:enclose_value)
128
+ "#{attribute} IN (#{escaped_values.join(',')})"
129
+ end
130
+
131
+ def eq_predicate(attribute, value)
132
+ "#{attribute} = #{enclose_value value}"
133
+ end
134
+
135
+ def enclose_value value
136
+ case value
137
+ when String
138
+ "'#{quote_string(value)}'"
139
+ when NilClass
140
+ 'NULL'
141
+ else
142
+ value.to_s
143
+ end
144
+ end
145
+
146
+ def quote_string(s)
147
+ # From activerecord/lib/active_record/connection_adapters/abstract/quoting.rb, version 4.1.5, line 82
148
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''")
149
+ end
150
+
151
+ def result
152
+ sfdc_client.query(self.to_s)
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveForce
2
+ module Association
3
+ class Association
4
+ extend Forwardable
5
+ def_delegators :relation_model, :build
6
+
7
+ attr_accessor :options, :relation_name
8
+
9
+ def initialize parent, relation_name, options = {}
10
+ @parent = parent
11
+ @relation_name = relation_name
12
+ @options = options
13
+ define_relation_method
14
+ end
15
+
16
+ def relation_model
17
+ options[:model] || relation_name.to_s.singularize.camelcase.constantize
18
+ end
19
+
20
+ def foreign_key
21
+ options[:foreign_key] || default_foreign_key
22
+ end
23
+
24
+ def relationship_name
25
+ options[:relationship_name] || relation_model.table_name
26
+ end
27
+
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
35
+ end
36
+
37
+ def sfdc_association_field
38
+ relationship_name.gsub /__c\z/, '__r'
39
+ end
40
+
41
+ private
42
+
43
+ def infer_foreign_key_from_model(model)
44
+ name = model.custom_table? ? model.name : model.table_name
45
+ name.foreign_key.to_sym
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveForce
2
+ module Association
3
+ class BelongsToAssociation < Association
4
+ private
5
+
6
+ def default_foreign_key
7
+ infer_foreign_key_from_model relation_model
8
+ end
9
+
10
+ def define_relation_method
11
+ association = self
12
+ _method = @relation_name
13
+ @parent.send :define_method, _method do
14
+ association_cache.fetch(_method) do
15
+ association_cache[_method] = association.relation_model.find(send association.foreign_key)
16
+ end
17
+ end
18
+
19
+ @parent.send :define_method, "#{_method}=" do |other|
20
+ send "#{ association.foreign_key }=", other.nil? ? nil : other.id
21
+ association_cache[_method] = other
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end