activesalesforce 0.4.0 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/asf_adapter.rb +59 -54
- data/lib/id_resolver.rb +2 -7
- data/lib/recording_binding.rb +90 -0
- data/lib/rforce.rb +2 -0
- data/lib/sid_authentication_filter.rb +22 -1
- data/test/unit/recorded_test_case.rb +7 -25
- metadata +3 -3
- data/lib/mock_binding.rb +0 -70
data/lib/asf_adapter.rb
CHANGED
@@ -1,23 +1,23 @@
|
|
1
1
|
=begin
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright 2006 Doug Chasman
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
16
|
=end
|
17
17
|
|
18
18
|
require 'rubygems'
|
19
19
|
require_gem 'rails', ">= 1.0.0"
|
20
|
-
|
20
|
+
|
21
21
|
require 'thread'
|
22
22
|
require 'benchmark'
|
23
23
|
|
@@ -29,6 +29,7 @@ require File.dirname(__FILE__) + '/entity_definition'
|
|
29
29
|
require File.dirname(__FILE__) + '/asf_active_record'
|
30
30
|
require File.dirname(__FILE__) + '/id_resolver'
|
31
31
|
require File.dirname(__FILE__) + '/sid_authentication_filter'
|
32
|
+
require File.dirname(__FILE__) + '/recording_binding'
|
32
33
|
|
33
34
|
|
34
35
|
class ResultArray < Array
|
@@ -51,12 +52,19 @@ module ActiveRecord
|
|
51
52
|
# Default to production system using 7.0 API
|
52
53
|
url = config[:url]
|
53
54
|
url = "https://www.salesforce.com/services/Soap/u/7.0" unless url
|
54
|
-
|
55
|
+
|
55
56
|
sid = config[:sid]
|
56
|
-
|
57
|
+
username = config[:username]
|
58
|
+
password = config[:password]
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
+
# Recording/playback support
|
61
|
+
recording_source = config[:recording_source]
|
62
|
+
recording = config[:recording]
|
63
|
+
|
64
|
+
if recording_source
|
65
|
+
recording_source = File.open(recording_source, recording ? "w" : "r")
|
66
|
+
binding = ActiveSalesforce::RecordingBinding.new(url, nil, recording != nil, recording_source, logger)
|
67
|
+
binding.login(username, password) unless sid
|
60
68
|
end
|
61
69
|
|
62
70
|
if sid
|
@@ -74,10 +82,7 @@ module ActiveRecord
|
|
74
82
|
ConnectionAdapters::SalesforceAdapter.new(binding, logger, [url, sid], config)
|
75
83
|
else
|
76
84
|
# Check to insure that the second to last path component is a 'u' for Partner API
|
77
|
-
raise
|
78
|
-
|
79
|
-
username = config[:username]
|
80
|
-
password = config[:password]
|
85
|
+
raise ActiveSalesforce::ASFError.new(logger, "Invalid salesforce server url '#{url}', must be a valid Parter API URL") unless url.match(/\/u\//i)
|
81
86
|
|
82
87
|
binding = @@cache["#{url}.#{username}.#{password}"] unless binding
|
83
88
|
|
@@ -86,7 +91,7 @@ module ActiveRecord
|
|
86
91
|
|
87
92
|
seconds = Benchmark.realtime {
|
88
93
|
binding = RForce::Binding.new(url, sid)
|
89
|
-
binding.login(username, password)
|
94
|
+
binding.login(username, password)
|
90
95
|
|
91
96
|
@@cache["#{url}.#{username}.#{password}"] = binding
|
92
97
|
}
|
@@ -98,25 +103,13 @@ module ActiveRecord
|
|
98
103
|
end
|
99
104
|
end
|
100
105
|
end
|
101
|
-
|
106
|
+
|
102
107
|
|
103
108
|
module ConnectionAdapters
|
104
|
-
class SalesforceError < RuntimeError
|
105
|
-
attr :fault
|
106
|
-
|
107
|
-
def initialize(logger, message, fault = nil)
|
108
|
-
super message
|
109
|
-
|
110
|
-
@fault = fault
|
111
|
-
|
112
|
-
logger.debug("\nSalesforceError:\n message='#{message}'\n fault='#{fault}'\n\n")
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
109
|
|
117
110
|
class SalesforceAdapter < AbstractAdapter
|
118
111
|
include StringHelper
|
119
|
-
|
112
|
+
|
120
113
|
MAX_BOXCAR_SIZE = 200
|
121
114
|
|
122
115
|
attr_accessor :batch_size
|
@@ -221,7 +214,7 @@ module ActiveRecord
|
|
221
214
|
unless errors.empty?
|
222
215
|
message = errors.join("\n")
|
223
216
|
fault = (errors.map { |error| error[:message] }).join("\n")
|
224
|
-
|
217
|
+
ActiveSalesforce::ASFError.new(@logger, message, fault)
|
225
218
|
end
|
226
219
|
|
227
220
|
result
|
@@ -293,7 +286,7 @@ module ActiveRecord
|
|
293
286
|
column_name.sub!(/\w+\./, '')
|
294
287
|
|
295
288
|
column = columns[column_name]
|
296
|
-
raise
|
289
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{column_name}!") unless column
|
297
290
|
|
298
291
|
column.api_name
|
299
292
|
end
|
@@ -440,7 +433,16 @@ module ActiveRecord
|
|
440
433
|
result = [ result ] unless result.is_a?(Array)
|
441
434
|
|
442
435
|
# Remove unwanted :type and normalize :Id if required
|
443
|
-
|
436
|
+
field_values = []
|
437
|
+
result.each do |v|
|
438
|
+
v = v.dup
|
439
|
+
v.delete(:type)
|
440
|
+
v[:Id] = v[:Id][0] if v[:Id].is_a? Array
|
441
|
+
|
442
|
+
field_values << v
|
443
|
+
end
|
444
|
+
|
445
|
+
field_values
|
444
446
|
}
|
445
447
|
end
|
446
448
|
|
@@ -453,7 +455,7 @@ module ActiveRecord
|
|
453
455
|
if value
|
454
456
|
column = columns[name]
|
455
457
|
|
456
|
-
raise
|
458
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name}!") unless column
|
457
459
|
|
458
460
|
value.gsub!(/''/, "'") if value.is_a? String
|
459
461
|
|
@@ -470,7 +472,7 @@ module ActiveRecord
|
|
470
472
|
responseName = (method.to_s + "Response").to_sym
|
471
473
|
finalResponse = response[responseName]
|
472
474
|
|
473
|
-
raise
|
475
|
+
raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
|
474
476
|
|
475
477
|
result = finalResponse[:result]
|
476
478
|
end
|
@@ -480,7 +482,7 @@ module ActiveRecord
|
|
480
482
|
result = [ result ] unless result.is_a?(Array)
|
481
483
|
|
482
484
|
result.each do |r|
|
483
|
-
raise
|
485
|
+
raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
|
484
486
|
end
|
485
487
|
|
486
488
|
result
|
@@ -494,7 +496,7 @@ module ActiveRecord
|
|
494
496
|
# Check for the loss of asf AR setup
|
495
497
|
entity_klass = class_from_entity_name(entity_name)
|
496
498
|
|
497
|
-
configure_active_record
|
499
|
+
configure_active_record(cached_entity_def) unless entity_klass.respond_to?(:asf_augmented?)
|
498
500
|
|
499
501
|
return cached_entity_def
|
500
502
|
end
|
@@ -506,7 +508,7 @@ module ActiveRecord
|
|
506
508
|
begin
|
507
509
|
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
|
508
510
|
custom = false
|
509
|
-
rescue
|
511
|
+
rescue ActiveSalesforce::ASFError => e
|
510
512
|
# Fallback and see if we can find a custom object with this name
|
511
513
|
@logger.debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
|
512
514
|
|
@@ -536,8 +538,8 @@ module ActiveRecord
|
|
536
538
|
key_prefix = metadata[:keyPrefix]
|
537
539
|
|
538
540
|
entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name,
|
539
|
-
|
540
|
-
|
541
|
+
cached_columns, cached_relationships, custom, key_prefix)
|
542
|
+
|
541
543
|
@entity_def_map[entity_name] = entity_def
|
542
544
|
@keyprefix_to_entity_def_map[key_prefix] = entity_def
|
543
545
|
|
@@ -558,6 +560,9 @@ module ActiveRecord
|
|
558
560
|
end
|
559
561
|
end
|
560
562
|
|
563
|
+
# Add support for SID-based authentication
|
564
|
+
ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
|
565
|
+
|
561
566
|
klass.set_inheritance_column nil
|
562
567
|
klass.set_primary_key "id"
|
563
568
|
klass.lock_optimistically = false
|
@@ -625,12 +630,12 @@ module ActiveRecord
|
|
625
630
|
|
626
631
|
|
627
632
|
def class_from_entity_name(entity_name)
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
633
|
+
entity_klass = @class_to_entity_map[entity_name.upcase]
|
634
|
+
@logger.debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
|
635
|
+
|
636
|
+
entity_klass = entity_name.constantize unless entity_klass
|
637
|
+
|
638
|
+
entity_klass
|
634
639
|
end
|
635
640
|
|
636
641
|
|
data/lib/id_resolver.rb
CHANGED
@@ -23,6 +23,7 @@ require 'pp'
|
|
23
23
|
|
24
24
|
module ActiveSalesforce
|
25
25
|
class IdResolver
|
26
|
+
attr_reader :object_type_to_ids
|
26
27
|
|
27
28
|
def initialize(connection)
|
28
29
|
@connection = connection
|
@@ -43,13 +44,7 @@ module ActiveSalesforce
|
|
43
44
|
|
44
45
|
value = record.send(column.name)
|
45
46
|
if value
|
46
|
-
ids = @object_type_to_ids[reference_to]
|
47
|
-
|
48
|
-
unless ids
|
49
|
-
ids = Set.new
|
50
|
-
@object_type_to_ids[reference_to] = ids
|
51
|
-
end
|
52
|
-
|
47
|
+
ids = @object_type_to_ids[reference_to] ||= Set.new
|
53
48
|
ids << value
|
54
49
|
end
|
55
50
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright 2006 Doug Chasman
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
=end
|
17
|
+
|
18
|
+
require 'yaml'
|
19
|
+
|
20
|
+
|
21
|
+
module ActiveSalesforce
|
22
|
+
|
23
|
+
class ASFError < RuntimeError
|
24
|
+
attr :fault
|
25
|
+
|
26
|
+
def initialize(logger, message, fault = nil)
|
27
|
+
super message
|
28
|
+
|
29
|
+
@fault = fault
|
30
|
+
|
31
|
+
logger.debug("\nASFError:\n message='#{message}'\n fault='#{fault}'\n\n")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
class ASFRecordingBindingError < ASFError
|
37
|
+
def initialize(logger, message)
|
38
|
+
super(logger, message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
class RecordingBinding < RForce::Binding
|
44
|
+
|
45
|
+
attr_reader :recorded_responses
|
46
|
+
|
47
|
+
def initialize(url, sid, recording, recording_source, logger)
|
48
|
+
super(url, sid)
|
49
|
+
|
50
|
+
@recording = recording
|
51
|
+
@recorded_responses = {}
|
52
|
+
@recording_source = recording_source
|
53
|
+
@logger = logger
|
54
|
+
|
55
|
+
unless @recording
|
56
|
+
YAML.load_documents(recording_source) do |recorded_response|
|
57
|
+
@recorded_responses.merge!(recorded_response)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
#Call a method on the remote server. Arguments can be
|
64
|
+
#a hash or (if order is important) an array of alternating
|
65
|
+
#keys and values.
|
66
|
+
def call_remote(method, args)
|
67
|
+
# Blank out username and password
|
68
|
+
safe_args = args.inject([]) {|memo, v| memo << ((memo.last == :username or memo.last == :password) ? "" : v) }
|
69
|
+
key = "#{method}(#{safe_args.join(':')})"
|
70
|
+
|
71
|
+
if @recording
|
72
|
+
response = super(method, args)
|
73
|
+
|
74
|
+
unless @recorded_responses[key]
|
75
|
+
# track this { key => request } to avoid duplication in the YAML
|
76
|
+
@recorded_responses[key] = true
|
77
|
+
|
78
|
+
YAML.dump({ key, response }, @recording_source)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
response = @recorded_responses[key]
|
82
|
+
raise ASFRecordingBindingError.new(@logger, "Unable to find matching response for recorded request '#{key}'") unless response
|
83
|
+
end
|
84
|
+
|
85
|
+
response
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
data/lib/rforce.rb
CHANGED
@@ -16,6 +16,7 @@
|
|
16
16
|
=end
|
17
17
|
|
18
18
|
require 'rubygems'
|
19
|
+
require 'set'
|
19
20
|
require_gem 'rails', ">= 1.0.0"
|
20
21
|
|
21
22
|
require 'pp'
|
@@ -24,17 +25,37 @@ require 'pp'
|
|
24
25
|
module ActiveSalesforce
|
25
26
|
|
26
27
|
class SessionIDAuthenticationFilter
|
28
|
+
@@klasses = Set.new
|
27
29
|
|
30
|
+
|
31
|
+
def self.register(klass)
|
32
|
+
@@klasses.add(klass)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
28
36
|
def self.filter(controller)
|
29
37
|
# Look to see if a SID was passed in the URL
|
30
38
|
params = controller.params
|
31
39
|
sid = params[:sid]
|
40
|
+
|
41
|
+
puts "Checking for the presence of a SID for [#{@@klasses.to_a.join(', ')}]"
|
42
|
+
|
32
43
|
if sid
|
33
44
|
api_server_url = params[:api_server_url]
|
34
45
|
api_server_url = 'http://na1-api.salesforce.com/services/Soap/u/7.0' unless api_server_url
|
35
46
|
|
36
47
|
puts "asf_sid_authenticate(:sid => '#{sid}', :url => '#{api_server_url}')"
|
37
|
-
|
48
|
+
|
49
|
+
# Iterate over all classes that have registered for SID auth support
|
50
|
+
connection = nil
|
51
|
+
@@klasses.each do |klass|
|
52
|
+
unless connection
|
53
|
+
klass.establish_connection(:adapter => 'activesalesforce', :sid => sid, :url => api_server_url)
|
54
|
+
connection = klass.connection
|
55
|
+
else
|
56
|
+
klass = connection
|
57
|
+
end
|
58
|
+
end
|
38
59
|
end
|
39
60
|
end
|
40
61
|
|
@@ -31,9 +31,7 @@ module Asf
|
|
31
31
|
LOGGER = Logger.new(STDOUT)
|
32
32
|
@@config = YAML.load_file(File.dirname(__FILE__) + '/config.yml').symbolize_keys
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
|
37
35
|
def recording?
|
38
36
|
@recording
|
39
37
|
end
|
@@ -66,36 +64,20 @@ module Asf
|
|
66
64
|
|
67
65
|
@recording = (((not File.exists?(recording_file_name)) or config[:recording]) or @force_recording.include?(method_name.to_sym))
|
68
66
|
|
69
|
-
|
67
|
+
action = { :adapter => 'activesalesforce', :username => config[:username],
|
68
|
+
:password => config[:password], :recording_source => recording_file_name, }
|
69
|
+
|
70
|
+
action[:recording] = true if @recording
|
70
71
|
|
71
72
|
ActiveRecord::Base.logger = LOGGER
|
72
73
|
ActiveRecord::Base.clear_connection_cache!
|
73
74
|
ActiveRecord::Base.reset_column_information_and_inheritable_attributes_for_all_subclasses
|
74
|
-
ActiveRecord::Base.establish_connection(
|
75
|
-
:password => config[:password], :binding => connection)
|
76
|
-
|
77
|
-
unless recording?
|
78
|
-
File.open(recording_file_name) do |f|
|
79
|
-
puts "Opening recorded binding #{recording_file_name}"
|
80
|
-
connection.load(f)
|
81
|
-
end
|
82
|
-
end
|
75
|
+
ActiveRecord::Base.establish_connection(action)
|
83
76
|
|
84
|
-
|
77
|
+
@connection = ActiveRecord::Base.connection
|
85
78
|
end
|
86
79
|
|
87
80
|
|
88
|
-
def teardown
|
89
|
-
if recording?
|
90
|
-
puts "Saving recorded binding #{recording_file_name}"
|
91
|
-
|
92
|
-
File.open(recording_file_name, "w") do |f|
|
93
|
-
connection.save(f)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
|
99
81
|
def recording_file_name
|
100
82
|
File.dirname(__FILE__) + "/recorded_results/#{self.class.name.gsub('::', '')}.#{method_name}.recording"
|
101
83
|
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
|
|
3
3
|
specification_version: 1
|
4
4
|
name: activesalesforce
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.4.
|
7
|
-
date: 2006-03-
|
6
|
+
version: 0.4.2
|
7
|
+
date: 2006-03-03 00:00:00 -05:00
|
8
8
|
summary: ActiveSalesforce (ASF) is a Rails connection adapter that provides direct access to Salesforce.com hosted data and metadata via the ActiveRecord model layer. Objects, fields, and relationships are all auto surfaced as active record attributes and rels.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -29,10 +29,10 @@ authors:
|
|
29
29
|
- Doug Chasman
|
30
30
|
files:
|
31
31
|
- lib/boxcar_command.rb
|
32
|
+
- lib/recording_binding.rb
|
32
33
|
- lib/column_definition.rb
|
33
34
|
- lib/activesalesforce.rb
|
34
35
|
- lib/entity_definition.rb
|
35
|
-
- lib/mock_binding.rb
|
36
36
|
- lib/rforce.rb
|
37
37
|
- lib/asf_adapter.rb
|
38
38
|
- lib/sid_authentication_filter.rb
|
data/lib/mock_binding.rb
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
=begin
|
2
|
-
ActiveSalesforce
|
3
|
-
Copyright 2006 Doug Chasman
|
4
|
-
|
5
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
-
you may not use this file except in compliance with the License.
|
7
|
-
You may obtain a copy of the License at
|
8
|
-
|
9
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
-
|
11
|
-
Unless required by applicable law or agreed to in writing, software
|
12
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
-
See the License for the specific language governing permissions and
|
15
|
-
limitations under the License.
|
16
|
-
=end
|
17
|
-
|
18
|
-
require 'yaml'
|
19
|
-
require File.dirname(__FILE__) + '/rforce'
|
20
|
-
|
21
|
-
|
22
|
-
class MockBinding < RForce::Binding
|
23
|
-
attr_reader :recorded_responses
|
24
|
-
|
25
|
-
#Connect to the server securely.
|
26
|
-
def initialize(url, sid, recording)
|
27
|
-
@recording = recording
|
28
|
-
@recorded_responses = {}
|
29
|
-
|
30
|
-
super(url, sid) if @recording
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
def save(f)
|
35
|
-
YAML.dump(@recorded_responses, f)
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
def load(f)
|
40
|
-
@recorded_responses = YAML.load(f)
|
41
|
-
end
|
42
|
-
|
43
|
-
|
44
|
-
#Call a method on the remote server. Arguments can be
|
45
|
-
#a hash or (if order is important) an array of alternating
|
46
|
-
#keys and values.
|
47
|
-
def call_remote(method, args)
|
48
|
-
# Blank out username and password
|
49
|
-
safe_args = args.inject([]) {|memo, v| memo << ((memo.last == :username or memo.last == :password) ? "" : v) }
|
50
|
-
key = "#{method}(#{safe_args.join(':')})"
|
51
|
-
|
52
|
-
if @recording
|
53
|
-
response = super(method, args)
|
54
|
-
@recorded_responses[key] = response
|
55
|
-
else
|
56
|
-
response = @recorded_responses[key]
|
57
|
-
|
58
|
-
unless response
|
59
|
-
@recorded_responses.each do |request, reponse|
|
60
|
-
#pp request
|
61
|
-
end
|
62
|
-
|
63
|
-
raise "Unable to find matching response for recorded request '#{key}'"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
response
|
68
|
-
end
|
69
|
-
|
70
|
-
end
|