fn-salesforce 0.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Guardfile +53 -0
- data/LICENSE.txt +21 -0
- data/README.md +147 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/fn-salesforce +85 -0
- data/bin/setup +7 -0
- data/docs/README.md +85 -0
- data/fn-salesforce.gemspec +42 -0
- data/lib/fn/salesforce.rb +84 -0
- data/lib/fn/salesforce/cli.rb +75 -0
- data/lib/fn/salesforce/environment.rb +75 -0
- data/lib/fn/salesforce/object.rb +14 -0
- data/lib/fn/salesforce/object_factory.rb +37 -0
- data/lib/fn/salesforce/options.rb +51 -0
- data/lib/fn/salesforce/version.rb +5 -0
- data/lib/fn/salesforce/walker.rb +43 -0
- metadata +217 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 56c8050e52f4dd1c6895fd3e89c12eeab60e8f19
|
4
|
+
data.tar.gz: 7ade6ae5a5a2fbaa5da19dd9d8de761b5e16e769
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 94bde1299413a5afd4612cb0e632a54cc453a9fff90b0e2d493a71e1e4d19843cb495852a7694870f28839f421e83d8ced4196d4fba84d86fd56b6f1ef6a3ffe
|
7
|
+
data.tar.gz: c0a9885db5ac8165562a582854d252de327c23c7260cd11aa5c79f7597273335d928e383f0abe77177329ff4a7d5e99857ae3355a6783e8d4004ff23aded4505
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.2
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
2
|
+
# rspec may be run, below are examples of the most common uses.
|
3
|
+
# * bundler: 'bundle exec rspec'
|
4
|
+
# * bundler binstubs: 'bin/rspec'
|
5
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
6
|
+
# installed the spring binstubs per the docs)
|
7
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
8
|
+
# * 'just' rspec: 'rspec'
|
9
|
+
|
10
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
11
|
+
require "guard/rspec/dsl"
|
12
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
13
|
+
|
14
|
+
# Feel free to open issues for suggestions and improvements
|
15
|
+
|
16
|
+
# RSpec files
|
17
|
+
rspec = dsl.rspec
|
18
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
19
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
20
|
+
watch(rspec.spec_files)
|
21
|
+
|
22
|
+
# Ruby files
|
23
|
+
ruby = dsl.ruby
|
24
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
25
|
+
|
26
|
+
# Rails files
|
27
|
+
rails = dsl.rails(view_extensions: %w(erb haml slim))
|
28
|
+
dsl.watch_spec_files_for(rails.app_files)
|
29
|
+
dsl.watch_spec_files_for(rails.views)
|
30
|
+
|
31
|
+
watch(rails.controllers) do |m|
|
32
|
+
[
|
33
|
+
rspec.spec.("routing/#{m[1]}_routing"),
|
34
|
+
rspec.spec.("controllers/#{m[1]}_controller"),
|
35
|
+
rspec.spec.("acceptance/#{m[1]}")
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Rails config changes
|
40
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
41
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
42
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
43
|
+
|
44
|
+
# Capybara features specs
|
45
|
+
watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
|
46
|
+
watch(rails.layouts) { |m| rspec.spec.("features/#{m[1]}") }
|
47
|
+
|
48
|
+
# Turnip features and steps
|
49
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
50
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
51
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
52
|
+
end
|
53
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Stuart Corbishley
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# OpenFn Salesforce Adaptor
|
2
|
+
|
3
|
+
*pre-release*
|
4
|
+
|
5
|
+
CLI Tool and Adaptor for working with Salesforce.
|
6
|
+
|
7
|
+
Intended to be used in conjunction with OpenFn, but has been designed
|
8
|
+
with standalone use in mind.
|
9
|
+
|
10
|
+
Leveraging JSON-Schema, fn-salesforce acts as a bridge between Salesforce
|
11
|
+
and external data. It supports nested JSON data, and can resolve
|
12
|
+
basic dependency graphs while providing access to intermediary data
|
13
|
+
formats.
|
14
|
+
|
15
|
+
|
16
|
+
Artifacts
|
17
|
+
---------
|
18
|
+
|
19
|
+
* **Credentials**
|
20
|
+
In order to communicate with Salesforce, a set of API credentials should
|
21
|
+
be available at processing time.
|
22
|
+
|
23
|
+
* **Destination Message**
|
24
|
+
Generic JSON document provided to the Saleforce adaptor.
|
25
|
+
|
26
|
+
* **Prepared Messages / Plan**
|
27
|
+
Intermediary JSON document used internally to represent the order and
|
28
|
+
the unfulfilled dependent values at the time of execution.
|
29
|
+
|
30
|
+
The array of objects received are assumed to be in a reliable order.
|
31
|
+
|
32
|
+
* **Schema**
|
33
|
+
JSON Schema document used to assemble a plan, linking up the SObject names
|
34
|
+
with the relationship keys found on the destination message.
|
35
|
+
|
36
|
+
- - -
|
37
|
+
|
38
|
+
## prepare
|
39
|
+
|
40
|
+
Flattens out a message into an array of objects to be sent to Salesforce.
|
41
|
+
|
42
|
+
See the **push** interface below.
|
43
|
+
|
44
|
+
```
|
45
|
+
fn-salesforce prepare -v -s schema.json payload.json 1> plan.json
|
46
|
+
```
|
47
|
+
|
48
|
+
|
49
|
+
## push
|
50
|
+
|
51
|
+
Using a prepared message, the push step sends the objects in order to
|
52
|
+
Salesforce and fills in `$ref` values just in time.
|
53
|
+
|
54
|
+
Example message:
|
55
|
+
|
56
|
+
```js
|
57
|
+
[
|
58
|
+
{
|
59
|
+
"sObject": "my__ObjectName__c",
|
60
|
+
"properties": {
|
61
|
+
"Id": "12345"
|
62
|
+
}
|
63
|
+
},
|
64
|
+
{
|
65
|
+
"sObject": "my__ObjectName__c",
|
66
|
+
"properties": {
|
67
|
+
"my__Custom_Reference__c": {
|
68
|
+
"$ref": "/0/properties/Id"
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
]
|
73
|
+
```
|
74
|
+
|
75
|
+
`$ref` values use [JSON Pointers](https://tools.ietf.org/html/rfc6901)
|
76
|
+
as a lookup value. At the time of resolving the reference, the message
|
77
|
+
list will only contain the objects before it.
|
78
|
+
|
79
|
+
If you need to look ahead, it's best solved in the original data source.
|
80
|
+
So it's actually only useful in the scenario where it's impossible to know
|
81
|
+
the value without talking to Salesforce first.
|
82
|
+
|
83
|
+
The array of objects received are assumed to be in a reliable order.
|
84
|
+
|
85
|
+
## describe
|
86
|
+
|
87
|
+
Returns the object description from Salesforce.
|
88
|
+
|
89
|
+
```
|
90
|
+
$ fn-salesforce describe -c ./.credentials.json -v my__Custom_Object__c
|
91
|
+
```
|
92
|
+
|
93
|
+
It's a pretty big JSON document, so you will probably want to send it to
|
94
|
+
a file using a redirect.
|
95
|
+
|
96
|
+
`$ fn-salesforce describe ... 1>object_description.json`
|
97
|
+
|
98
|
+
|
99
|
+
CLI
|
100
|
+
---
|
101
|
+
|
102
|
+
`$ fn-salesforce COMMAND [options] [subject]`
|
103
|
+
|
104
|
+
There are a few switches used to configure the CLI client, every command
|
105
|
+
needs login credentials to communicate with Salesforce.
|
106
|
+
|
107
|
+
- `-c` `--credentials`
|
108
|
+
Provide a credentials file for communicating with Salesforce.
|
109
|
+
- `-v` `--verbose`
|
110
|
+
Show extra detail when executing commands.
|
111
|
+
|
112
|
+
`$ fn-salesforce push PAYLOAD`
|
113
|
+
|
114
|
+
Process a JSON document and create Salesforce objects.
|
115
|
+
|
116
|
+
Example
|
117
|
+
|
118
|
+
`fn-salesforce push payload.json`
|
119
|
+
|
120
|
+
`$ fn-salesforce describe OBJECT`
|
121
|
+
|
122
|
+
Describes an object. Returns JSON by default.
|
123
|
+
|
124
|
+
Example
|
125
|
+
|
126
|
+
`fn-salesforce describe User`
|
127
|
+
|
128
|
+
Tests
|
129
|
+
-----
|
130
|
+
|
131
|
+
`rspec`
|
132
|
+
|
133
|
+
|
134
|
+
## Development
|
135
|
+
|
136
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
137
|
+
Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
138
|
+
|
139
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
140
|
+
|
141
|
+
## Contributing
|
142
|
+
|
143
|
+
1. Fork it ( https://github.com/[my-github-username]/fn-salesforce/fork )
|
144
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
145
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
146
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
147
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "fn/salesforce"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
|
+
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start
|
data/bin/fn-salesforce
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
require 'fn/salesforce'
|
8
|
+
require 'fn/salesforce/cli'
|
9
|
+
|
10
|
+
options = {}
|
11
|
+
|
12
|
+
base = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: fn-salesforce COMMAND [options] [subject]"
|
14
|
+
|
15
|
+
opts.separator ""
|
16
|
+
opts.separator <<HELP
|
17
|
+
Commonly used command are:
|
18
|
+
push : Push a JSON document to Salesforce
|
19
|
+
describe : Describe a Salesforce object
|
20
|
+
See 'fn-salesforce COMMAND --help' for more information on a specific command.
|
21
|
+
HELP
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
commands = {
|
26
|
+
prepare: OptionParser.new { |opts|
|
27
|
+
opts.banner = "Usage: prepare [options] PAYLOAD"
|
28
|
+
opts.on("-sSCHEMA", "--schema SCHEMA", "JSON-Schema description of payload") do |n|
|
29
|
+
options[:schema] = n
|
30
|
+
end
|
31
|
+
opts.on("-v", "--verbose", "Enable verbose output") do |v|
|
32
|
+
options[:verbose] = v
|
33
|
+
end
|
34
|
+
},
|
35
|
+
push: OptionParser.new { |opts|
|
36
|
+
opts.banner = "Usage: push [options] PLAN"
|
37
|
+
opts.on("-cCREDENTIALS", "--credentials CREDENTIALS", "API Credentials for Salesforce") do |f|
|
38
|
+
options[:credentials] = f
|
39
|
+
end
|
40
|
+
opts.on("-v", "--verbose", "Enable verbose output") do |v|
|
41
|
+
options[:verbose] = v
|
42
|
+
end
|
43
|
+
},
|
44
|
+
describe: OptionParser.new { |opts|
|
45
|
+
options[:format] = "json"
|
46
|
+
|
47
|
+
opts.banner = "Usage: describe [options] OBJECT"
|
48
|
+
opts.on("-cCREDENTIALS", "--credentials CREDENTIALS", "API Credentials for Salesforce") do |f|
|
49
|
+
options[:credentials] = f
|
50
|
+
end
|
51
|
+
opts.on("-f", "--format", "Output format (json, raw, schema)") do |f|
|
52
|
+
options[:format] = f
|
53
|
+
end
|
54
|
+
opts.on("-v", "--verbose", "Enable verbose output") do |v|
|
55
|
+
options[:verbose] = v
|
56
|
+
end
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
command_key = (ARGV.shift || "").to_sym
|
61
|
+
command = commands[command_key] || base
|
62
|
+
|
63
|
+
command.parse!
|
64
|
+
|
65
|
+
case command_key
|
66
|
+
when :describe
|
67
|
+
|
68
|
+
options.merge!( {target:ARGV.slice(-1)} )
|
69
|
+
Fn::Salesforce::CLI.describe(options)
|
70
|
+
|
71
|
+
when :push
|
72
|
+
|
73
|
+
options.merge!( {plan:ARGV.slice(-1)} )
|
74
|
+
Fn::Salesforce::CLI.push(options)
|
75
|
+
|
76
|
+
when :prepare
|
77
|
+
|
78
|
+
options.merge!( {message:ARGV.slice(-1)} )
|
79
|
+
Fn::Salesforce::CLI.prepare(options)
|
80
|
+
|
81
|
+
else
|
82
|
+
puts command
|
83
|
+
end
|
84
|
+
|
85
|
+
|
data/bin/setup
ADDED
data/docs/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
Creating Child Objects
|
2
|
+
======================
|
3
|
+
|
4
|
+
In order to handle nested objects, we key the children by the relationship
|
5
|
+
name.
|
6
|
+
|
7
|
+
We need to make the schema available to the parser, so that the parent's ID
|
8
|
+
can be placed in the correct field on the child record.
|
9
|
+
|
10
|
+
```js
|
11
|
+
{
|
12
|
+
"type" : "object",
|
13
|
+
"$schema" : "http://json-schema.org/draft-04/schema#",
|
14
|
+
"title" : "Test Event",
|
15
|
+
"properties" : {
|
16
|
+
"vera__Attendance__r" : {
|
17
|
+
"type" : "array",
|
18
|
+
"id" : "vera__Attendance__c"
|
19
|
+
},
|
20
|
+
|
21
|
+
// We can't rely on the key names as the destination objects location
|
22
|
+
// since an object may have multiple FK associations to the same object.
|
23
|
+
// So it's important that we key by the relationship.
|
24
|
+
|
25
|
+
// That means that we have to discover the object name for the given
|
26
|
+
// key.
|
27
|
+
|
28
|
+
// i.e. `vera__Boats__r` and `vera__Boats_Frilitanger__r` both
|
29
|
+
// are associated with `vera__Boat__c`, and have different FKs.
|
30
|
+
|
31
|
+
"vera__Boats__r" : {
|
32
|
+
// At this node we are reflecting the 'relationship'
|
33
|
+
"type" : "array",
|
34
|
+
|
35
|
+
// NOTE: This is likely to change.
|
36
|
+
// Need to have a look around how other projects handle 'metadata'
|
37
|
+
|
38
|
+
// For now we can use the `foreignKey` handle to describe which key to use
|
39
|
+
// when the primary key is available.
|
40
|
+
"foreignKey" : "vera__Third_Event__c",
|
41
|
+
"sObject" : "vera__Boat__c",
|
42
|
+
|
43
|
+
|
44
|
+
"items" : {
|
45
|
+
"type" : "object",
|
46
|
+
"properties" : {
|
47
|
+
"vera__Third_Event__c": { "type": "string" },
|
48
|
+
"vera__Boat_Maker__c": { "type": "string" }
|
49
|
+
}
|
50
|
+
}
|
51
|
+
},
|
52
|
+
|
53
|
+
|
54
|
+
"vera__Boats_Frilitanger__r" : {
|
55
|
+
"type" : "array",
|
56
|
+
"id" : "vera__Boat__c",
|
57
|
+
"foreignKey" : "vera__Test_Event__c"
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
```
|
63
|
+
|
64
|
+
Scenario
|
65
|
+
--------
|
66
|
+
|
67
|
+
**TODO: write out expectation around parsing against a schema**
|
68
|
+
Given: foo schema, bar message
|
69
|
+
When: parsing bar
|
70
|
+
Then: expect array of template ready objects, linked by reference.
|
71
|
+
|
72
|
+
Using JSONPath style selectors, we inspect the embedded data on the schema for
|
73
|
+
that given property.
|
74
|
+
|
75
|
+
We use template string values to set up the child objects foreign key value.
|
76
|
+
|
77
|
+
```js
|
78
|
+
{ "vera__Third_Event__c": "{parent.Id}" }
|
79
|
+
```
|
80
|
+
|
81
|
+
The idea is to have perform parse and template the values out.
|
82
|
+
|
83
|
+
The reasoning behind this 2 step approach is so that we test and review
|
84
|
+
outputs without hitting the servers. A core focus for these tools is to assist
|
85
|
+
in debugging and testing as much as possible.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fn/salesforce/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fn-salesforce"
|
8
|
+
spec.version = Fn::Salesforce::VERSION
|
9
|
+
spec.authors = ["Stuart Corbishley"]
|
10
|
+
spec.email = ["corbish@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Salesforce support adaptor for OpenFn}
|
13
|
+
spec.description = <<-EOF
|
14
|
+
CLI Tool and Adaptor for working with Salesforce.
|
15
|
+
|
16
|
+
Intended to be used in conjunction with OpenFn, but has been designed
|
17
|
+
with standalone use in mind.
|
18
|
+
|
19
|
+
Leveraging JSON-Schema, fn-salesforce acts as a bridge between Salesforce
|
20
|
+
and external data. It supports nested JSON data, and can resolve
|
21
|
+
basic dependency graphs while providing access to intermediary data
|
22
|
+
formats.
|
23
|
+
EOF
|
24
|
+
spec.homepage = "https://github.com/stuartc/fn-salesforce"
|
25
|
+
spec.license = "MIT"
|
26
|
+
|
27
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
28
|
+
spec.bindir = "bin"
|
29
|
+
spec.executables = [ "fn-salesforce" ]
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "guard-rspec"
|
35
|
+
spec.add_development_dependency "pry-byebug"
|
36
|
+
spec.add_development_dependency "rake-notes"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.3.0"
|
38
|
+
spec.add_dependency "restforce", "~> 2.1.0"
|
39
|
+
spec.add_dependency "jsonpath", "~> 0.5.7"
|
40
|
+
spec.add_dependency "hana", "~> 1.3.1"
|
41
|
+
spec.add_dependency "virtus", "~> 1.0.5"
|
42
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "json"
|
2
|
+
require "fn/salesforce/version"
|
3
|
+
require "fn/salesforce/environment"
|
4
|
+
require "fn/salesforce/options"
|
5
|
+
require "fn/salesforce/object"
|
6
|
+
require "fn/salesforce/object_factory"
|
7
|
+
require "fn/salesforce/walker"
|
8
|
+
require "restforce"
|
9
|
+
require "hana"
|
10
|
+
|
11
|
+
module Fn
|
12
|
+
module Salesforce
|
13
|
+
|
14
|
+
# Push
|
15
|
+
# ----
|
16
|
+
# Takes a prepared payload, and sends it to Salesforce.
|
17
|
+
def self.push(credentials, message)
|
18
|
+
|
19
|
+
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
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Prepare
|
57
|
+
# -------
|
58
|
+
# Unpack the message, and return a data object ready for sending.
|
59
|
+
def self.prepare(schema, message)
|
60
|
+
factory = Fn::Salesforce::ObjectFactory.new(schema)
|
61
|
+
|
62
|
+
raw_payload = []
|
63
|
+
Walker.parse(message) do |key, properties, parent|
|
64
|
+
obj = factory.create(key, properties, parent)
|
65
|
+
$stderr.puts obj
|
66
|
+
raw_payload << obj
|
67
|
+
|
68
|
+
raw_payload.index obj
|
69
|
+
end
|
70
|
+
|
71
|
+
raw_payload
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
# Describe
|
76
|
+
# --------
|
77
|
+
def self.describe(credentials, target)
|
78
|
+
|
79
|
+
client = Restforce.new(credentials)
|
80
|
+
client.describe(target)
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'pry-byebug'
|
2
|
+
module Fn::Salesforce
|
3
|
+
class CLI
|
4
|
+
class << self
|
5
|
+
|
6
|
+
# TODO: Move options validation from bin to CLI class
|
7
|
+
def push(opts,schema,file)
|
8
|
+
begin
|
9
|
+
payload = JSON.parse(File.read(file))
|
10
|
+
schema = JSON.parse(File.read(schema))
|
11
|
+
|
12
|
+
pp Fn::Salesforce.push({}, schema, payload)
|
13
|
+
|
14
|
+
rescue => e
|
15
|
+
logger = Logger.new(STDERR)
|
16
|
+
logger.level = Logger::DEBUG
|
17
|
+
logger.error e
|
18
|
+
logger.debug e.backtrace.join("\n") if opts[:verbose]
|
19
|
+
ensure
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def prepare(opts)
|
26
|
+
Environment.new(opts) do
|
27
|
+
logger.debug "Preparing"
|
28
|
+
|
29
|
+
prepare(schema, message) { |plan|
|
30
|
+
puts JSON.pretty_generate plan
|
31
|
+
}
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
exit(0)
|
36
|
+
end
|
37
|
+
|
38
|
+
def describe(opts)
|
39
|
+
Environment.new(opts) do
|
40
|
+
logger.debug "Describing: #{target}"
|
41
|
+
logger.debug "Credentials:"
|
42
|
+
logger.debug credentials
|
43
|
+
|
44
|
+
describe(credentials, target) { |description|
|
45
|
+
case format
|
46
|
+
when 'raw'
|
47
|
+
puts description
|
48
|
+
when 'json'
|
49
|
+
puts JSON.pretty_generate description
|
50
|
+
end
|
51
|
+
}
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
exit(0)
|
56
|
+
end
|
57
|
+
|
58
|
+
def push(opts)
|
59
|
+
Environment.new(opts) do
|
60
|
+
logger.debug "Pushing to Salesforce"
|
61
|
+
logger.debug "Credentials:"
|
62
|
+
logger.debug credentials
|
63
|
+
|
64
|
+
push(credentials, plan) { |results|
|
65
|
+
pp results
|
66
|
+
}
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
exit(0)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Fn Salesforce Environment
|
2
|
+
# =========================
|
3
|
+
#
|
4
|
+
# Interface for performing operations, sets up the logger and provides
|
5
|
+
# access to configuration objects.
|
6
|
+
#
|
7
|
+
# Since using the tool via the CLI and API requires slightly different
|
8
|
+
# handling for errors and feedback, this can be used to facilitate that.
|
9
|
+
|
10
|
+
require 'logger'
|
11
|
+
require 'forwardable'
|
12
|
+
|
13
|
+
# Fn::Salesforce::Environment.new({schema: "SCHEMA"}) do
|
14
|
+
# begin
|
15
|
+
# prepare(schema, payload) { |plan|
|
16
|
+
# raise "hell"
|
17
|
+
# }
|
18
|
+
# rescue
|
19
|
+
# logger.error "Hello"
|
20
|
+
# exit(1)
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
|
24
|
+
class Fn::Salesforce::Environment
|
25
|
+
extend Forwardable
|
26
|
+
def_delegators :@options, :credentials, :schema, :payload, :target,
|
27
|
+
:format, :message, :plan
|
28
|
+
|
29
|
+
attr_reader :logger
|
30
|
+
|
31
|
+
def initialize(options, &block)
|
32
|
+
@options = Fn::Salesforce::Options.new(options)
|
33
|
+
@logger = Logger.new(STDERR)
|
34
|
+
@logger.level = Logger::DEBUG
|
35
|
+
|
36
|
+
instance_eval &block
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# ## Prepare
|
41
|
+
# ```rb
|
42
|
+
# prepare(schema, payload) { |plan|
|
43
|
+
# puts plan.inspect
|
44
|
+
# logger.info plan.inspect
|
45
|
+
# }
|
46
|
+
# ```
|
47
|
+
|
48
|
+
def prepare(schema, payload, &block)
|
49
|
+
yield Fn::Salesforce.prepare(schema, payload)
|
50
|
+
end
|
51
|
+
|
52
|
+
# ## Describe
|
53
|
+
# ```rb
|
54
|
+
# describe(credentials, target) { |description|
|
55
|
+
# pp description.inspect
|
56
|
+
# }
|
57
|
+
# ```
|
58
|
+
|
59
|
+
def describe(credentials, target, &block)
|
60
|
+
yield Fn::Salesforce.describe(credentials, target)
|
61
|
+
end
|
62
|
+
|
63
|
+
# ## Push
|
64
|
+
# ```rb
|
65
|
+
# push(credentials, plan) { |results|
|
66
|
+
# pp results
|
67
|
+
# }
|
68
|
+
# ```
|
69
|
+
|
70
|
+
def push(credentials, plan, &block)
|
71
|
+
yield Fn::Salesforce.push(credentials, plan)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
class Fn::Salesforce::Object
|
4
|
+
include Virtus.model(:finalize => false)
|
5
|
+
end
|
6
|
+
|
7
|
+
class Fn::Salesforce::Object
|
8
|
+
attribute :sobject, String
|
9
|
+
attribute :parent, Fn::Salesforce::Object
|
10
|
+
attribute :properties, Hash
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
Virtus.finalize
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# ObjectFactory
|
2
|
+
# =============
|
3
|
+
#
|
4
|
+
# Factory class to help reconcile source data and the target schema.
|
5
|
+
#
|
6
|
+
# By providing a JSON schema, that describes the relationships between
|
7
|
+
# objects when traversing the message structure, we can prepare an object
|
8
|
+
# for sending.
|
9
|
+
|
10
|
+
require 'jsonpath'
|
11
|
+
|
12
|
+
class Fn::Salesforce::ObjectFactory
|
13
|
+
|
14
|
+
attr_reader :schema
|
15
|
+
def initialize(schema)
|
16
|
+
raise ArgumentError, "Schema must be a hash." unless schema.is_a? Hash
|
17
|
+
@schema = schema
|
18
|
+
end
|
19
|
+
|
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
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def sobject_for(key)
|
31
|
+
JsonPath.on(schema,"..properties.#{key}.sObject").first
|
32
|
+
end
|
33
|
+
|
34
|
+
def foreign_key_for(relationship_key)
|
35
|
+
JsonPath.on(schema,"..properties.#{relationship_key}.foreignKey").first
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
class Fn::Salesforce::Options
|
4
|
+
|
5
|
+
def initialize(attributes)
|
6
|
+
@attributes = attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
def credentials
|
11
|
+
return @credentials if @credentials
|
12
|
+
|
13
|
+
credentials = JSON.parse(File.read(@attributes[:credentials]))
|
14
|
+
@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"]
|
21
|
+
} ).to_hash(symbolize_keys: true)
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def schema
|
26
|
+
return @schema if @schema
|
27
|
+
|
28
|
+
@schema = JSON.parse(File.read(@attributes[:schema]))
|
29
|
+
end
|
30
|
+
|
31
|
+
def message
|
32
|
+
return @message if @message
|
33
|
+
|
34
|
+
@message = JSON.parse(File.read(@attributes[:message]))
|
35
|
+
end
|
36
|
+
|
37
|
+
def plan
|
38
|
+
return @plan if @plan
|
39
|
+
|
40
|
+
@plan = JSON.parse(File.read(@attributes[:plan]))
|
41
|
+
end
|
42
|
+
|
43
|
+
def target
|
44
|
+
@attributes[:target]
|
45
|
+
end
|
46
|
+
|
47
|
+
def format
|
48
|
+
@attributes[:format]
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Payload Tree Walker
|
2
|
+
# ===================
|
3
|
+
#
|
4
|
+
# Object Emitter for JSON payloads.
|
5
|
+
class Fn::Salesforce::Walker
|
6
|
+
|
7
|
+
# Parse
|
8
|
+
# -----
|
9
|
+
#
|
10
|
+
# ```ruby
|
11
|
+
# counter = 0
|
12
|
+
# parse(tree) do |key,properties|
|
13
|
+
# puts key
|
14
|
+
# puts properties
|
15
|
+
#
|
16
|
+
# # The return result is passed back to the caller,
|
17
|
+
# # this way we can pass down the resulting `id` after the parent
|
18
|
+
# # object has been created.
|
19
|
+
# counter+= 1
|
20
|
+
# end
|
21
|
+
# ```
|
22
|
+
|
23
|
+
def self.parse(tree, parent=nil, &block)
|
24
|
+
tree.each { |obj,properties|
|
25
|
+
# We assume all properties that belong to this object are not arrays.
|
26
|
+
immediate_properties = properties.select { |k,v| !v.is_a?(Array) }
|
27
|
+
|
28
|
+
# Without the child objects present, we send it out for processing.
|
29
|
+
# Grab the result and carry on.
|
30
|
+
result = block.call(obj, immediate_properties, parent)
|
31
|
+
|
32
|
+
children = properties.select { |k,v| v.is_a?(Array) } \
|
33
|
+
# We map out the array of child objects to single key/value hashes.
|
34
|
+
.collect { |k,v| v.map { |v| {k => v} } }.flatten \
|
35
|
+
# And send them off individually for processing.
|
36
|
+
.each { |child|
|
37
|
+
# The handle to the direct parent's result is made available.
|
38
|
+
parse(child, result, &block)
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fn-salesforce
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stuart Corbishley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard-rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry-byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake-notes
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.3.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.3.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: restforce
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.1.0
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.1.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: jsonpath
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.5.7
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.5.7
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: hana
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 1.3.1
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 1.3.1
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: virtus
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.0.5
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 1.0.5
|
153
|
+
description: |2
|
154
|
+
CLI Tool and Adaptor for working with Salesforce.
|
155
|
+
|
156
|
+
Intended to be used in conjunction with OpenFn, but has been designed
|
157
|
+
with standalone use in mind.
|
158
|
+
|
159
|
+
Leveraging JSON-Schema, fn-salesforce acts as a bridge between Salesforce
|
160
|
+
and external data. It supports nested JSON data, and can resolve
|
161
|
+
basic dependency graphs while providing access to intermediary data
|
162
|
+
formats.
|
163
|
+
email:
|
164
|
+
- corbish@gmail.com
|
165
|
+
executables:
|
166
|
+
- fn-salesforce
|
167
|
+
extensions: []
|
168
|
+
extra_rdoc_files: []
|
169
|
+
files:
|
170
|
+
- ".gitignore"
|
171
|
+
- ".rspec"
|
172
|
+
- ".ruby-version"
|
173
|
+
- ".travis.yml"
|
174
|
+
- CODE_OF_CONDUCT.md
|
175
|
+
- Gemfile
|
176
|
+
- Guardfile
|
177
|
+
- LICENSE.txt
|
178
|
+
- README.md
|
179
|
+
- Rakefile
|
180
|
+
- bin/console
|
181
|
+
- bin/fn-salesforce
|
182
|
+
- bin/setup
|
183
|
+
- docs/README.md
|
184
|
+
- fn-salesforce.gemspec
|
185
|
+
- lib/fn/salesforce.rb
|
186
|
+
- lib/fn/salesforce/cli.rb
|
187
|
+
- lib/fn/salesforce/environment.rb
|
188
|
+
- lib/fn/salesforce/object.rb
|
189
|
+
- lib/fn/salesforce/object_factory.rb
|
190
|
+
- lib/fn/salesforce/options.rb
|
191
|
+
- lib/fn/salesforce/version.rb
|
192
|
+
- lib/fn/salesforce/walker.rb
|
193
|
+
homepage: https://github.com/stuartc/fn-salesforce
|
194
|
+
licenses:
|
195
|
+
- MIT
|
196
|
+
metadata: {}
|
197
|
+
post_install_message:
|
198
|
+
rdoc_options: []
|
199
|
+
require_paths:
|
200
|
+
- lib
|
201
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
207
|
+
requirements:
|
208
|
+
- - ">"
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: 1.3.1
|
211
|
+
requirements: []
|
212
|
+
rubyforge_project:
|
213
|
+
rubygems_version: 2.4.5
|
214
|
+
signing_key:
|
215
|
+
specification_version: 4
|
216
|
+
summary: Salesforce support adaptor for OpenFn
|
217
|
+
test_files: []
|