fn-salesforce 0.1.0.pre → 0.2.0.pre

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 56c8050e52f4dd1c6895fd3e89c12eeab60e8f19
4
- data.tar.gz: 7ade6ae5a5a2fbaa5da19dd9d8de761b5e16e769
3
+ metadata.gz: 82f216e7b819a1f7b0a01c9d6c5e01ba488bae78
4
+ data.tar.gz: ab224735759865beefcb5a3ccd1d8fd5e59f6ecd
5
5
  SHA512:
6
- metadata.gz: 94bde1299413a5afd4612cb0e632a54cc453a9fff90b0e2d493a71e1e4d19843cb495852a7694870f28839f421e83d8ced4196d4fba84d86fd56b6f1ef6a3ffe
7
- data.tar.gz: c0a9885db5ac8165562a582854d252de327c23c7260cd11aa5c79f7597273335d928e383f0abe77177329ff4a7d5e99857ae3355a6783e8d4004ff23aded4505
6
+ metadata.gz: 8cf89d992698421808b9a31831e0d7cd949b8c201391bbdb6451c6645ec7413a8d9130f483743937d73a4ee2c17802e0b270ff66564b467051fdb0a80053d6b7
7
+ data.tar.gz: b51b76d9286030d2411c668d869f026b55dcf99299e71939b24c40c2b9039f9d25eb10dcd051ef77d9cedbe6b2433ca6d1386f844529ffd09e0dc795e0b4ef18
data/README.md CHANGED
@@ -12,6 +12,10 @@ and external data. It supports nested JSON data, and can resolve
12
12
  basic dependency graphs while providing access to intermediary data
13
13
  formats.
14
14
 
15
+ Install
16
+ -------
17
+
18
+ `$ gem install fn-salesforce --pre`
15
19
 
16
20
  Artifacts
17
21
  ---------
@@ -82,6 +86,34 @@ the value without talking to Salesforce first.
82
86
 
83
87
  The array of objects received are assumed to be in a reliable order.
84
88
 
89
+ ### Message types
90
+
91
+ **create**
92
+
93
+ ```js
94
+ {
95
+ "sObject": "my__ObjectName__c",
96
+ "properties": {
97
+ "Id": "12345"
98
+ }
99
+ }
100
+ ```
101
+
102
+ **upsert**
103
+
104
+ ```js
105
+ {
106
+ "sObject": "my__ObjectName__c",
107
+ "properties": {
108
+ "Foo": "Bar",
109
+ "External_Baz__c": "Catatafish"
110
+ },
111
+ "action": "upsert",
112
+ "externalID": "External_Baz__c"
113
+ }
114
+ ```
115
+
116
+
85
117
  ## describe
86
118
 
87
119
  Returns the object description from Salesforce.
data/lib/fn/salesforce.rb CHANGED
@@ -2,7 +2,9 @@ require "json"
2
2
  require "fn/salesforce/version"
3
3
  require "fn/salesforce/environment"
4
4
  require "fn/salesforce/options"
5
- require "fn/salesforce/object"
5
+ require "fn/salesforce/operation"
6
+ require "fn/salesforce/rollback"
7
+ require "fn/salesforce/transaction"
6
8
  require "fn/salesforce/object_factory"
7
9
  require "fn/salesforce/walker"
8
10
  require "restforce"
@@ -14,43 +16,20 @@ module Fn
14
16
  # Push
15
17
  # ----
16
18
  # Takes a prepared payload, and sends it to Salesforce.
17
- def self.push(credentials, message)
19
+ def self.push(credentials, message, logger: Logger.new(STDOUT))
18
20
 
19
21
  client = Restforce.new(credentials)
20
- message.
21
- inject([]) { |plan,obj|
22
-
23
- begin
24
- obj["properties"].merge! obj["properties"].
25
- map { |k,v|
26
- if v.is_a?(Hash) && v["$ref"]
27
- v = Hana::Pointer.new(v["$ref"]).eval(plan)
28
- end
29
- [k,v]
30
- }.
31
- map { |i| Hash[*i] }.inject(&:merge)
32
-
33
- $stderr.puts "Creating #{obj["sObject"]}: #{ obj["properties"] }"
34
- id = client.create!(obj["sObject"], obj["properties"])
35
-
36
- obj["properties"]["Id"] = id
37
- plan.push obj
38
-
39
- rescue Exception => e
40
- $stderr.puts e
41
- if plan.any?
42
- $stderr.puts "Rolling back previous objects"
43
- plan.each { |obj|
44
- id = obj["properties"]["Id"]
45
- object = obj["sObject"]
46
- $stderr.puts "Deleting #{object}##{id}"
47
- client.destroy(object, id)
48
- }
49
- end
50
- break
51
- end
52
-
53
- }
22
+ plan = Plan.new(message)
23
+
24
+ transaction = Transaction.new(client, plan, logger: logger)
25
+
26
+ transaction.execute
27
+
28
+ transaction.rollback! if transaction.failed
29
+
30
+ # Return true for success, false for failure.
31
+ !transaction.failed
32
+
54
33
  end
55
34
 
56
35
  # Prepare
@@ -18,19 +18,44 @@ class Fn::Salesforce::ObjectFactory
18
18
  end
19
19
 
20
20
  def create(key, properties, parent=nil)
21
- sobject = sobject_for(key) || key
22
- properties.merge! Hash[foreign_key_for(key), {"$ref" => "/#{parent}/properties/Id"}] if parent
23
-
24
- {
25
- "sObject" => sobject,
26
- "properties" => properties
27
- }
21
+
22
+ [
23
+ ->(obj) {
24
+ { "properties" => properties }
25
+ },
26
+ ->(obj) {
27
+ { "action" => "create" }
28
+ },
29
+ ->(obj) {
30
+ { "sObject" => sobject_for(key) || key }
31
+ },
32
+ ->(obj) {
33
+ return {} unless parent
34
+ {
35
+ "properties" => obj["properties"].merge(
36
+ Hash[ foreign_key_for(key), {"$ref" => "/#{parent}/Id"} ]
37
+ )
38
+ }
39
+ },
40
+ ->(obj) {
41
+ return {} unless lookup_key = lookup_key_for(key)
42
+ {
43
+ "action" => "update",
44
+ "lookupWith" => Hash[lookup_key, obj["properties"][lookup_key]],
45
+ "properties" => obj["properties"].select { |k| k != lookup_key }
46
+ }
47
+ }
48
+ ].reduce({}) { |memo,op| memo.merge op[memo] }
28
49
  end
29
50
 
30
51
  def sobject_for(key)
31
52
  JsonPath.on(schema,"..properties.#{key}.sObject").first
32
53
  end
33
54
 
55
+ def lookup_key_for(key)
56
+ JsonPath.on(schema,"..properties.#{key}.lookupKey").first
57
+ end
58
+
34
59
  def foreign_key_for(relationship_key)
35
60
  JsonPath.on(schema,"..properties.#{relationship_key}.foreignKey").first
36
61
  end
@@ -0,0 +1,51 @@
1
+ require 'delegate'
2
+
3
+ module Fn
4
+ module Salesforce
5
+
6
+ class Operation < Hashie::Mash
7
+
8
+ def action
9
+ (super || :create ).to_sym
10
+ end
11
+
12
+ def s_object
13
+ sObject
14
+ end
15
+
16
+ def lookup_with
17
+ lookupWith
18
+ end
19
+
20
+ def previous_properties
21
+ previousProperties
22
+ end
23
+
24
+ def references
25
+ properties.select { |k,v|
26
+ v.is_a? Hash
27
+ }.select { |k,v|
28
+ v.keys.any? { |key| key == "$ref" }
29
+ }
30
+ end
31
+
32
+ def replace_refs(data)
33
+ properties.merge! properties.
34
+ map { |k,v|
35
+ if v.is_a?(Hash) && v["$ref"]
36
+ v = Hana::Pointer.new(v["$ref"]).eval(data)
37
+ end
38
+ [k,v]
39
+ }.
40
+ map { |i| Hash[*i] }.inject(&:merge)
41
+ end
42
+ end
43
+
44
+ class Plan < SimpleDelegator
45
+ def initialize(operations)
46
+ super(operations.map { |op| Operation.new(op) })
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -10,16 +10,20 @@ class Fn::Salesforce::Options
10
10
  def credentials
11
11
  return @credentials if @credentials
12
12
 
13
- credentials = JSON.parse(File.read(@attributes[:credentials]))
13
+ if @attributes[:credentials].is_a? String
14
+ attributes = JSON.parse(File.read(@attributes[:credentials]))
15
+ else
16
+ attributes = @attributes[:credentials]
17
+ end
18
+
14
19
  @credentials = Hashie::Mash.new( {
15
- username: credentials["username"],
16
- password: credentials["password"],
17
- security_token: credentials["token"],
18
- client_id: credentials["key"],
19
- client_secret: credentials["secret"],
20
- host: credentials["host"]
20
+ username: attributes["username"],
21
+ password: attributes["password"],
22
+ security_token: attributes["token"],
23
+ client_id: attributes["key"],
24
+ client_secret: attributes["secret"],
25
+ host: attributes["host"]
21
26
  } ).to_hash(symbolize_keys: true)
22
-
23
27
  end
24
28
 
25
29
  def schema
@@ -0,0 +1,29 @@
1
+ module Fn
2
+ module Salesforce
3
+ class Rollback < SimpleDelegator
4
+
5
+ def initialize(plan)
6
+ super(
7
+ plan.map { |operation|
8
+ operation.dup
9
+ }.map { |operation| UndoFactory.coerce(operation) }
10
+ )
11
+ end
12
+
13
+ end
14
+
15
+ class UndoFactory
16
+ def self.coerce(operation)
17
+ case operation.action
18
+ when :create
19
+ operation["action"] = "delete"
20
+ when :update
21
+ operation["properties"] = operation.previous_properties
22
+ end
23
+
24
+ operation
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,89 @@
1
+ module Fn
2
+ module Salesforce
3
+
4
+ class Transaction
5
+
6
+ attr_reader :client, :plan, :failed, :result
7
+ attr_accessor :logger
8
+
9
+ def initialize(client, plan, logger: Logger.new(STDOUT))
10
+ @logger = logger
11
+ @client = client
12
+ @plan = plan
13
+ @result = []
14
+ @failed = false
15
+ end
16
+
17
+ def execute
18
+ @result = plan.inject([]) { |results, operation|
19
+ begin
20
+ operation.replace_refs(results)
21
+ results << perform(operation)
22
+ rescue Exception => e
23
+ logger.error "Transaction failed at ##{plan.index(operation)}"
24
+ logger.error "Error message: #{e.message}"
25
+ @failed = true
26
+ break results
27
+ end
28
+ results
29
+ }
30
+ end
31
+
32
+
33
+ def rollback!
34
+ Rollback.new(result).each { |operation| perform(operation) }
35
+ end
36
+
37
+ private
38
+
39
+ def perform(operation)
40
+ case operation.action
41
+
42
+ when :create
43
+ logger.info "Creating #{operation.s_object}: #{ operation.properties.to_hash }"
44
+ id = client.create!( operation.s_object, operation.properties.dup )
45
+ operation.merge!( {"Id" => id} )
46
+ when :update
47
+ logger.info "Finding #{operation.s_object}: #{ operation.lookup_with.to_hash }"
48
+ object = client.find(
49
+ operation.s_object,
50
+ *operation.lookup_with.invert.to_a.flatten
51
+ )
52
+
53
+ logger.info "Found #{operation.s_object}(#{object.Id})"
54
+ # Keep the Id we get back from the find operation, we
55
+ # use this for updating the object, and for rolling back if needed.
56
+ operation.merge!( {"Id" => object.Id} )
57
+
58
+ # Slice off the fields we are about to change, and put them on
59
+ # 'previousProperties'.
60
+ operation.previousProperties = object.select { |key, value|
61
+ operation.properties.keys.include?(key)
62
+ }
63
+ logger.info "Updating #{operation.s_object}: #{ operation.properties.to_hash }"
64
+
65
+ client.update!(
66
+ operation.s_object,
67
+ Hash["Id", operation["Id"]].merge( operation.properties )
68
+ )
69
+
70
+ when :delete
71
+ logger.info "Deleting #{operation.s_object}(#{operation.Id})"
72
+ client.destroy!(operation.sObject, operation.Id)
73
+
74
+ when :upsert
75
+ logger.info "Upserting #{operation.s_object}(#{operation.externalID})"
76
+ client.upsert!(operation.sObject, operation.externalID, operation.properties)
77
+ else
78
+ logger.warn "Warning: No action found for #{operation.inspect}. Skipping."
79
+ end
80
+
81
+ operation
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+
88
+
89
+ end
@@ -1,5 +1,5 @@
1
1
  module Fn
2
2
  module Salesforce
3
- VERSION = "0.1.0.pre"
3
+ VERSION = "0.2.0.pre"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fn-salesforce
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre
4
+ version: 0.2.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stuart Corbishley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-14 00:00:00.000000000 Z
11
+ date: 2015-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -187,7 +187,10 @@ files:
187
187
  - lib/fn/salesforce/environment.rb
188
188
  - lib/fn/salesforce/object.rb
189
189
  - lib/fn/salesforce/object_factory.rb
190
+ - lib/fn/salesforce/operation.rb
190
191
  - lib/fn/salesforce/options.rb
192
+ - lib/fn/salesforce/rollback.rb
193
+ - lib/fn/salesforce/transaction.rb
191
194
  - lib/fn/salesforce/version.rb
192
195
  - lib/fn/salesforce/walker.rb
193
196
  homepage: https://github.com/stuartc/fn-salesforce