sumskyi-activesalesforce 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
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 'activesalesforce_adapter'
19
+
20
+ module ActionView
21
+ module Helpers
22
+ # Provides a set of methods for making easy links and getting urls that depend on the controller and action. This means that
23
+ # you can use the same format for links in the views that you do in the controller. The different methods are even named
24
+ # synchronously, so link_to uses that same url as is generated by url_for, which again is the same url used for
25
+ # redirection in redirect_to.
26
+ module UrlHelper
27
+ def link_to_asf(active_record, column)
28
+ if column.reference_to
29
+ link_to(column.reference_to, { :action => 'show', :controller => column.reference_to.pluralize, :id => active_record.send(column.name) } )
30
+ else
31
+ active_record.send(column.name)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,780 @@
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 'thread'
19
+ require 'benchmark'
20
+
21
+ require 'active_record'
22
+ require 'active_record/connection_adapters/abstract_adapter'
23
+
24
+ require 'rforce'
25
+
26
+ require File.dirname(__FILE__) + '/column_definition'
27
+ require File.dirname(__FILE__) + '/relationship_definition'
28
+ require File.dirname(__FILE__) + '/boxcar_command'
29
+ require File.dirname(__FILE__) + '/entity_definition'
30
+ require File.dirname(__FILE__) + '/asf_active_record'
31
+ require File.dirname(__FILE__) + '/id_resolver'
32
+ require File.dirname(__FILE__) + '/sid_authentication_filter'
33
+ require File.dirname(__FILE__) + '/recording_binding'
34
+ require File.dirname(__FILE__) + '/result_array'
35
+
36
+
37
+ module ActiveRecord
38
+ class Base
39
+ @@cache = {}
40
+
41
+ def self.debug(msg)
42
+ logger.debug(msg) if logger
43
+ end
44
+
45
+ def self.flush_connections()
46
+ @@cache = {}
47
+ end
48
+
49
+ # Establishes a connection to the database that's used by all Active Record objects.
50
+ def self.activesalesforce_connection(config) # :nodoc:
51
+ debug("\nUsing ActiveSalesforce connection\n")
52
+
53
+ # Default to production system using 11.0 API
54
+ url = config[:url]
55
+ url = "https://www.salesforce.com" unless url
56
+
57
+ uri = URI.parse(url)
58
+ uri.path = "/services/Soap/u/11.0"
59
+ url = uri.to_s
60
+
61
+ sid = config[:sid]
62
+ client_id = config[:client_id]
63
+ username = config[:username].to_s
64
+ password = config[:password].to_s
65
+
66
+ # Recording/playback support
67
+ recording_source = config[:recording_source]
68
+ recording = config[:recording]
69
+
70
+ if recording_source
71
+ recording_source = File.open(recording_source, recording ? "w" : "r")
72
+ binding = ActiveSalesforce::RecordingBinding.new(url, nil, recording != nil, recording_source, logger)
73
+ binding.client_id = client_id if client_id
74
+ binding.login(username, password) unless sid
75
+ end
76
+
77
+ # Check to insure that the second to last path component is a 'u' for Partner API
78
+ raise ActiveSalesforce::ASFError.new(logger, "Invalid salesforce server url '#{url}', must be a valid Parter API URL") unless url.match(/\/u\//mi)
79
+
80
+ if sid
81
+ binding = @@cache["sid=#{sid}"] unless binding
82
+
83
+ unless binding
84
+ debug("Establishing new connection for [sid='#{sid}']")
85
+
86
+ binding = RForce::Binding.new(url, sid)
87
+ @@cache["sid=#{sid}"] = binding
88
+
89
+ debug("Created new connection for [sid='#{sid}']")
90
+ else
91
+ debug("Reused existing connection for [sid='#{sid}']")
92
+ end
93
+ else
94
+ binding = @@cache["#{url}.#{username}.#{password}.#{client_id}"] unless binding
95
+
96
+ unless binding
97
+ debug("Establishing new connection for ['#{url}', '#{username}, '#{client_id}'")
98
+
99
+ seconds = Benchmark.realtime {
100
+ binding = RForce::Binding.new(url, sid)
101
+ binding.login(username, password)
102
+
103
+ @@cache["#{url}.#{username}.#{password}.#{client_id}"] = binding
104
+ }
105
+
106
+ debug("Created new connection for ['#{url}', '#{username}', '#{client_id}'] in #{seconds} seconds")
107
+ end
108
+ end
109
+
110
+ ConnectionAdapters::SalesforceAdapter.new(binding, logger, config)
111
+
112
+ end
113
+ end
114
+
115
+
116
+ module ConnectionAdapters
117
+
118
+ class SalesforceAdapter < AbstractAdapter
119
+ include StringHelper
120
+
121
+ MAX_BOXCAR_SIZE = 200
122
+
123
+ attr_accessor :batch_size
124
+ attr_reader :entity_def_map, :keyprefix_to_entity_def_map, :config, :class_to_entity_map
125
+
126
+ def initialize(connection, logger, config)
127
+ super(connection, logger)
128
+
129
+ @connection_options = nil
130
+ @config = config
131
+
132
+ @entity_def_map = {}
133
+ @keyprefix_to_entity_def_map = {}
134
+
135
+ @command_boxcar = []
136
+ @class_to_entity_map = {}
137
+ end
138
+
139
+
140
+ def set_class_for_entity(klass, entity_name)
141
+ debug("Setting @class_to_entity_map['#{entity_name.upcase}'] = #{klass} for connection #{self}")
142
+ @class_to_entity_map[entity_name.upcase] = klass
143
+ end
144
+
145
+
146
+ def binding
147
+ @connection
148
+ end
149
+
150
+
151
+ def adapter_name #:nodoc:
152
+ 'ActiveSalesforce'
153
+ end
154
+
155
+
156
+ def supports_migrations? #:nodoc:
157
+ false
158
+ end
159
+
160
+
161
+ # QUOTING ==================================================
162
+
163
+ def quote(value, column = nil)
164
+ case value
165
+ when NilClass then quoted_value = "NULL"
166
+ when TrueClass then quoted_value = "TRUE"
167
+ when FalseClass then quoted_value = "FALSE"
168
+ when Float, Fixnum, Bignum then quoted_value = "'#{value.to_s}'"
169
+ else quoted_value = super(value, column)
170
+ end
171
+
172
+ quoted_value
173
+ end
174
+
175
+ # CONNECTION MANAGEMENT ====================================
176
+
177
+ def active?
178
+ true
179
+ end
180
+
181
+
182
+ def reconnect!
183
+ connect
184
+ end
185
+
186
+
187
+ # TRANSACTIOn SUPPORT (Boxcarring really because the salesforce.com api does not support transactions)
188
+
189
+ # Begins the transaction (and turns off auto-committing).
190
+ def begin_db_transaction()
191
+ log('Opening boxcar', 'begin_db_transaction()')
192
+ @command_boxcar = []
193
+ end
194
+
195
+
196
+ def send_commands(commands)
197
+ # Send the boxcar'ed command set
198
+ verb = commands[0].verb
199
+
200
+ args = []
201
+ commands.each do |command|
202
+ command.args.each { |arg| args << arg }
203
+ end
204
+
205
+ response = @connection.send(verb, args)
206
+
207
+ result = get_result(response, verb)
208
+
209
+ result = [ result ] unless result.is_a?(Array)
210
+
211
+ errors = []
212
+ result.each_with_index do |r, n|
213
+ success = r[:success] == "true"
214
+
215
+ # Give each command a chance to process its own result
216
+ command = commands[n]
217
+ command.after_execute(r)
218
+
219
+ # Handle the set of failures
220
+ errors << r[:errors] unless r[:success] == "true"
221
+ end
222
+
223
+ unless errors.empty?
224
+ message = errors.join("\n")
225
+ fault = (errors.map { |error| error[:message] }).join("\n")
226
+ raise ActiveSalesforce::ASFError.new(@logger, message, fault)
227
+ end
228
+
229
+ result
230
+ end
231
+
232
+
233
+ # Commits the transaction (and turns on auto-committing).
234
+ def commit_db_transaction()
235
+ log("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
236
+
237
+ previous_command = nil
238
+ commands = []
239
+
240
+ @command_boxcar.each do |command|
241
+ if commands.length >= MAX_BOXCAR_SIZE or (previous_command and (command.verb != previous_command.verb))
242
+ send_commands(commands)
243
+
244
+ commands = []
245
+ previous_command = nil
246
+ else
247
+ commands << command
248
+ previous_command = command
249
+ end
250
+ end
251
+
252
+ # Finish off the partial boxcar
253
+ send_commands(commands) unless commands.empty?
254
+
255
+ end
256
+
257
+ # Rolls back the transaction (and turns on auto-committing). Must be
258
+ # done if the transaction block raises an exception or returns false.
259
+ def rollback_db_transaction()
260
+ log('Rolling back boxcar', 'rollback_db_transaction()')
261
+ @command_boxcar = []
262
+ end
263
+
264
+
265
+ # DATABASE STATEMENTS ======================================
266
+
267
+ def select_all(sql, name = nil) #:nodoc:
268
+ raw_table_name = sql.match(/FROM (\w+)/mi)[1]
269
+ table_name, columns, entity_def = lookup(raw_table_name)
270
+
271
+ column_names = columns.map { |column| column.api_name }
272
+
273
+ # Check for SELECT COUNT(*) FROM query
274
+
275
+ # Rails 1.1
276
+ selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+AS\s+count_all\s+FROM/mi)
277
+
278
+ # Rails 1.0
279
+ selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+FROM/mi) unless selectCountMatch
280
+
281
+ if selectCountMatch
282
+ soql = "SELECT COUNT() FROM#{selectCountMatch.post_match}"
283
+ else
284
+ if sql.match(/SELECT\s+\*\s+FROM/mi)
285
+ # Always convert SELECT * to select all columns (required for the AR attributes mechanism to work correctly)
286
+ soql = sql.sub(/SELECT .+ FROM/mi, "SELECT #{column_names.join(', ')} FROM")
287
+ else
288
+ soql = sql
289
+ end
290
+ end
291
+
292
+ soql.sub!(/\s+FROM\s+\w+/mi, " FROM #{entity_def.api_name}")
293
+
294
+ if selectCountMatch
295
+ query_result = get_result(@connection.query(:queryString => soql), :query)
296
+ return [{ :count => query_result[:size] }]
297
+ end
298
+
299
+ # Look for a LIMIT clause
300
+ limit = extract_sql_modifier(soql, "LIMIT")
301
+ limit = MAX_BOXCAR_SIZE unless limit
302
+
303
+ # Look for an OFFSET clause
304
+ offset = extract_sql_modifier(soql, "OFFSET")
305
+
306
+ # Fixup column references to use api names
307
+ columns = entity_def.column_name_to_column
308
+ soql.gsub!(/((?:\w+\.)?\w+)(?=\s*(?:=|!=|<|>|<=|>=|like)\s*(?:'[^']*'|NULL|TRUE|FALSE))/mi) do |column_name|
309
+ # strip away any table alias
310
+ column_name.sub!(/\w+\./, '')
311
+
312
+ column = columns[column_name]
313
+ raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{column_name}!") unless column
314
+
315
+ column.api_name
316
+ end
317
+
318
+ # Update table name references
319
+ soql.sub!(/#{raw_table_name}\./mi, "#{entity_def.api_name}.")
320
+
321
+ @connection.batch_size = @batch_size if @batch_size
322
+ @batch_size = nil
323
+
324
+ query_result = get_result(@connection.query(:queryString => soql), :query)
325
+ result = ActiveSalesforce::ResultArray.new(query_result[:size].to_i)
326
+ return result unless query_result[:records]
327
+
328
+ add_rows(entity_def, query_result, result, limit)
329
+
330
+ while ((query_result[:done].casecmp("true") != 0) and (result.size < limit or limit == 0))
331
+ # Now queryMore
332
+ locator = query_result[:queryLocator];
333
+ query_result = get_result(@connection.queryMore(:queryLocator => locator), :queryMore)
334
+
335
+ add_rows(entity_def, query_result, result, limit)
336
+ end
337
+
338
+ result
339
+ end
340
+
341
+ def add_rows(entity_def, query_result, result, limit)
342
+ records = query_result[:records]
343
+ records = [ records ] unless records.is_a?(Array)
344
+
345
+ records.each do |record|
346
+ row = {}
347
+
348
+ record.each do |name, value|
349
+ if name != :type
350
+ # Ids may be returned in an array with 2 duplicate entries...
351
+ value = value[0] if name == :Id && value.is_a?(Array)
352
+
353
+ column = entity_def.api_name_to_column[name.to_s]
354
+ attribute_name = column.name
355
+
356
+ if column.type == :boolean
357
+ row[attribute_name] = (value.casecmp("true") == 0)
358
+ else
359
+ row[attribute_name] = value
360
+ end
361
+ end
362
+ end
363
+
364
+ result << row
365
+
366
+ break if result.size >= limit and limit != 0
367
+ end
368
+ end
369
+
370
+ def select_one(sql, name = nil) #:nodoc:
371
+ self.batch_size = 1
372
+
373
+ result = select_all(sql, name)
374
+
375
+ result.nil? ? nil : result.first
376
+ end
377
+
378
+
379
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
380
+ log(sql, name) {
381
+ # Convert sql to sobject
382
+ table_name, columns, entity_def = lookup(sql.match(/INSERT\s+INTO\s+(\w+)\s+/mi)[1])
383
+ columns = entity_def.column_name_to_column
384
+
385
+ # Extract array of column names
386
+ names = sql.match(/\((.+)\)\s+VALUES/mi)[1].scan(/\w+/mi)
387
+
388
+ # Extract arrays of values
389
+ values = sql.match(/VALUES\s*\((.+)\)/mi)[1]
390
+ values = values.scan(/(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
391
+ values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
392
+
393
+ fields = get_fields(columns, names, values, :createable)
394
+
395
+ sobject = create_sobject(entity_def.api_name, nil, fields)
396
+
397
+ # Track the id to be able to update it when the create() is actually executed
398
+ id = String.new
399
+ @command_boxcar << ActiveSalesforce::BoxcarCommand::Insert.new(self, sobject, id)
400
+
401
+ id
402
+ }
403
+ end
404
+
405
+
406
+ def update(sql, name = nil) #:nodoc:
407
+ #log(sql, name) {
408
+ # Convert sql to sobject
409
+ table_name, columns, entity_def = lookup(sql.match(/UPDATE\s+(\w+)\s+/mi)[1])
410
+ columns = entity_def.column_name_to_column
411
+
412
+ match = sql.match(/SET\s+(.+)\s+WHERE/mi)[1]
413
+ names = match.scan(/(\w+)\s*=\s*(?:'|NULL|TRUE|FALSE)/mi).flatten
414
+
415
+ values = match.scan(/=\s*(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
416
+ values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
417
+
418
+ fields = get_fields(columns, names, values, :updateable)
419
+ null_fields = get_null_fields(columns, names, values, :updateable)
420
+
421
+ id = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)[1]
422
+
423
+ sobject = create_sobject(entity_def.api_name, id, fields, null_fields)
424
+
425
+ @command_boxcar << ActiveSalesforce::BoxcarCommand::Update.new(self, sobject)
426
+ #}
427
+ end
428
+
429
+
430
+ def delete(sql, name = nil)
431
+ log(sql, name) {
432
+ # Extract the id
433
+ match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
434
+
435
+ if match
436
+ ids = [ match[1] ]
437
+ else
438
+ # Check for the form (id IN ('x', 'y'))
439
+ match = sql.match(/WHERE\s+\(\s*id\s+IN\s*\((.+)\)\)/mi)[1]
440
+ ids = match.scan(/\w+/)
441
+ end
442
+
443
+ ids_element = []
444
+ ids.each { |id| ids_element << :ids << id }
445
+
446
+ @command_boxcar << ActiveSalesforce::BoxcarCommand::Delete.new(self, ids_element)
447
+ }
448
+ end
449
+
450
+
451
+ def get_updated(object_type, start_date, end_date, name = nil)
452
+ msg = "get_updated(#{object_type}, #{start_date}, #{end_date})"
453
+ log(msg, name) {
454
+ get_updated_element = []
455
+ get_updated_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
456
+ get_updated_element << :startDate << start_date
457
+ get_updated_element << :endDate << end_date
458
+
459
+ result = get_result(@connection.getUpdated(get_updated_element), :getUpdated)
460
+
461
+ result[:ids]
462
+ }
463
+ end
464
+
465
+
466
+ def get_deleted(object_type, start_date, end_date, name = nil)
467
+ msg = "get_deleted(#{object_type}, #{start_date}, #{end_date})"
468
+ log(msg, name) {
469
+ get_deleted_element = []
470
+ get_deleted_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
471
+ get_deleted_element << :startDate << start_date
472
+ get_deleted_element << :endDate << end_date
473
+
474
+ result = get_result(@connection.getDeleted(get_deleted_element), :getDeleted)
475
+
476
+ ids = []
477
+ result[:deletedRecords].each do |v|
478
+ ids << v[:id]
479
+ end
480
+
481
+ ids
482
+ }
483
+ end
484
+
485
+
486
+ def get_user_info(name = nil)
487
+ msg = "get_user_info()"
488
+ log(msg, name) {
489
+ get_result(@connection.getUserInfo([]), :getUserInfo)
490
+ }
491
+ end
492
+
493
+
494
+ def retrieve_field_values(object_type, fields, ids, name = nil)
495
+ msg = "retrieve(#{object_type}, [#{ids.to_a.join(', ')}])"
496
+ log(msg, name) {
497
+ retrieve_element = []
498
+ retrieve_element << :fieldList << fields.to_a.join(", ")
499
+ retrieve_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
500
+ ids.to_a.each { |id| retrieve_element << :ids << id }
501
+
502
+ result = get_result(@connection.retrieve(retrieve_element), :retrieve)
503
+
504
+ result = [ result ] unless result.is_a?(Array)
505
+
506
+ # Remove unwanted :type and normalize :Id if required
507
+ field_values = []
508
+ result.each do |v|
509
+ v = v.dup
510
+ v.delete(:type)
511
+ v[:Id] = v[:Id][0] if v[:Id].is_a? Array
512
+
513
+ field_values << v
514
+ end
515
+
516
+ field_values
517
+ }
518
+ end
519
+
520
+
521
+ def get_fields(columns, names, values, access_check)
522
+ fields = {}
523
+ names.each_with_index do | name, n |
524
+ value = values[n]
525
+
526
+ if value
527
+ column = columns[name]
528
+
529
+ raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name}!") unless column
530
+
531
+ value.gsub!(/''/, "'") if value.is_a? String
532
+
533
+ include_field = ((not value.empty?) and column.send(access_check))
534
+
535
+ if (include_field)
536
+ case column.type
537
+ when :date
538
+ value = Time.parse(value + "Z").utc.strftime("%Y-%m-%d")
539
+ when :datetime
540
+ value = Time.parse(value + "Z").utc.strftime("%Y-%m-%dT%H:%M:%SZ")
541
+ end
542
+
543
+ fields[column.api_name] = value
544
+ end
545
+ end
546
+ end
547
+
548
+ fields
549
+ end
550
+
551
+ def get_null_fields(columns, names, values, access_check)
552
+ fields = {}
553
+ names.each_with_index do | name, n |
554
+ value = values[n]
555
+
556
+ if !value
557
+ column = columns[name]
558
+ fields[column.api_name] = nil if column.send(access_check) && column.api_name.casecmp("ownerid") != 0
559
+ end
560
+ end
561
+
562
+ fields
563
+ end
564
+
565
+ def extract_sql_modifier(soql, modifier)
566
+ value = soql.match(/\s+#{modifier}\s+(\d+)/mi)
567
+ if value
568
+ value = value[1].to_i
569
+ soql.sub!(/\s+#{modifier}\s+\d+/mi, "")
570
+ end
571
+
572
+ value
573
+ end
574
+
575
+
576
+ def get_result(response, method)
577
+ responseName = (method.to_s + "Response").to_sym
578
+ finalResponse = response[responseName]
579
+
580
+ raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
581
+
582
+ result = finalResponse[:result]
583
+ end
584
+
585
+
586
+ def check_result(result)
587
+ result = [ result ] unless result.is_a?(Array)
588
+
589
+ result.each do |r|
590
+ raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
591
+ end
592
+
593
+ result
594
+ end
595
+
596
+
597
+ def get_entity_def(entity_name)
598
+ cached_entity_def = @entity_def_map[entity_name]
599
+
600
+ if cached_entity_def
601
+ # Check for the loss of asf AR setup
602
+ entity_klass = class_from_entity_name(entity_name)
603
+
604
+ configure_active_record(cached_entity_def) unless entity_klass.respond_to?(:asf_augmented?)
605
+
606
+ return cached_entity_def
607
+ end
608
+
609
+ cached_columns = []
610
+ cached_relationships = []
611
+
612
+ begin
613
+ metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
614
+ custom = false
615
+ rescue ActiveSalesforce::ASFError
616
+ # Fallback and see if we can find a custom object with this name
617
+ debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
618
+
619
+ metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
620
+ custom = true
621
+ end
622
+
623
+ metadata[:fields].each do |field|
624
+ column = SalesforceColumn.new(field)
625
+ cached_columns << column
626
+
627
+ cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/mi
628
+ end
629
+
630
+ relationships = metadata[:childRelationships]
631
+ if relationships
632
+ relationships = [ relationships ] unless relationships.is_a? Array
633
+
634
+ relationships.each do |relationship|
635
+ if relationship[:cascadeDelete] == "true"
636
+ r = SalesforceRelationship.new(relationship)
637
+ cached_relationships << r
638
+ end
639
+ end
640
+ end
641
+
642
+ key_prefix = metadata[:keyPrefix]
643
+
644
+ entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name, entity_klass,
645
+ cached_columns, cached_relationships, custom, key_prefix)
646
+
647
+ @entity_def_map[entity_name] = entity_def
648
+ @keyprefix_to_entity_def_map[key_prefix] = entity_def
649
+
650
+ configure_active_record(entity_def)
651
+
652
+ entity_def
653
+ end
654
+
655
+
656
+ def configure_active_record(entity_def)
657
+ entity_name = entity_def.name
658
+ klass = class_from_entity_name(entity_name)
659
+
660
+ class << klass
661
+ def asf_augmented?
662
+ true
663
+ end
664
+ end
665
+
666
+ # Add support for SID-based authentication
667
+ ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
668
+
669
+ klass.set_inheritance_column nil unless entity_def.custom?
670
+ klass.set_primary_key "id"
671
+
672
+ # Create relationships for any reference field
673
+ entity_def.relationships.each do |relationship|
674
+ referenceName = relationship.name
675
+ unless self.respond_to? referenceName.to_sym or relationship.reference_to == "Profile"
676
+ reference_to = relationship.reference_to
677
+ one_to_many = relationship.one_to_many
678
+ foreign_key = relationship.foreign_key
679
+
680
+ # DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
681
+ # Account, Contact, Opportunity, Contract, Asset, Product2, <CustomObject1> ... <CustomObject(n)>
682
+ if reference_to.is_a? Array
683
+ debug(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
684
+ next
685
+ end
686
+
687
+ # Handle references to custom objects
688
+ reference_to = reference_to.chomp("__c").capitalize if reference_to.match(/__c$/)
689
+
690
+ begin
691
+ referenced_klass = class_from_entity_name(reference_to)
692
+ rescue NameError => e
693
+ # Automatically create a least a stub for the referenced entity
694
+ debug(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
695
+
696
+ referenced_klass = klass.class_eval("::#{reference_to} = Class.new(ActiveRecord::Base)")
697
+
698
+ # Automatically inherit the connection from the referencee
699
+ def referenced_klass.connection
700
+ klass.connection
701
+ end
702
+ end
703
+
704
+ if referenced_klass
705
+ if one_to_many
706
+ klass.has_many referenceName.to_sym, :class_name => referenced_klass.name, :foreign_key => foreign_key, :dependent => :nullify
707
+ else
708
+ klass.belongs_to referenceName.to_sym, :class_name => referenced_klass.name, :foreign_key => foreign_key
709
+ end
710
+
711
+ debug(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to #{referenced_klass} using #{foreign_key}")
712
+ end
713
+ end
714
+ end
715
+
716
+ end
717
+
718
+
719
+ def columns(table_name, name = nil)
720
+ table_name, columns, entity_def = lookup(table_name)
721
+ entity_def.columns
722
+ end
723
+
724
+
725
+ def class_from_entity_name(entity_name)
726
+ entity_klass = @class_to_entity_map[entity_name.upcase]
727
+ debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
728
+
729
+ entity_klass = entity_name.constantize unless entity_klass
730
+
731
+ entity_klass
732
+ end
733
+
734
+
735
+ def create_sobject(entity_name, id, fields, null_fields = [])
736
+ sobj = []
737
+
738
+ sobj << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << entity_name
739
+ sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << id if id
740
+
741
+ # add any changed fields
742
+ fields.each do | name, value |
743
+ sobj << name.to_sym << value if value
744
+ end
745
+
746
+ # add null fields
747
+ null_fields.each do | name, value |
748
+ sobj << 'fieldsToNull { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << name
749
+ end
750
+
751
+ [ :sObjects, sobj ]
752
+ end
753
+
754
+
755
+ def column_names(table_name)
756
+ columns(table_name).map { |column| column.name }
757
+ end
758
+
759
+
760
+ def lookup(raw_table_name)
761
+ table_name = raw_table_name.singularize
762
+
763
+ # See if a table name to AR class mapping was registered
764
+ klass = @class_to_entity_map[table_name.upcase]
765
+
766
+ entity_name = klass ? raw_table_name : table_name.camelize
767
+ entity_def = get_entity_def(entity_name)
768
+
769
+ [table_name, entity_def.columns, entity_def]
770
+ end
771
+
772
+
773
+ def debug(msg)
774
+ @logger.debug(msg) if @logger
775
+ end
776
+
777
+ end
778
+
779
+ end
780
+ end