workato-connector-sdk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +20 -0
  3. data/README.md +1093 -0
  4. data/exe/workato +5 -0
  5. data/lib/workato/cli/edit_command.rb +71 -0
  6. data/lib/workato/cli/exec_command.rb +191 -0
  7. data/lib/workato/cli/generate_command.rb +105 -0
  8. data/lib/workato/cli/generators/connector_generator.rb +94 -0
  9. data/lib/workato/cli/generators/master_key_generator.rb +56 -0
  10. data/lib/workato/cli/main.rb +142 -0
  11. data/lib/workato/cli/push_command.rb +208 -0
  12. data/lib/workato/connector/sdk/account_properties.rb +60 -0
  13. data/lib/workato/connector/sdk/action.rb +88 -0
  14. data/lib/workato/connector/sdk/block_invocation_refinements.rb +30 -0
  15. data/lib/workato/connector/sdk/connector.rb +230 -0
  16. data/lib/workato/connector/sdk/dsl/account_property.rb +15 -0
  17. data/lib/workato/connector/sdk/dsl/call.rb +17 -0
  18. data/lib/workato/connector/sdk/dsl/error.rb +15 -0
  19. data/lib/workato/connector/sdk/dsl/http.rb +60 -0
  20. data/lib/workato/connector/sdk/dsl/lookup_table.rb +15 -0
  21. data/lib/workato/connector/sdk/dsl/time.rb +21 -0
  22. data/lib/workato/connector/sdk/dsl/workato_code_lib.rb +105 -0
  23. data/lib/workato/connector/sdk/dsl/workato_schema.rb +15 -0
  24. data/lib/workato/connector/sdk/dsl.rb +46 -0
  25. data/lib/workato/connector/sdk/errors.rb +30 -0
  26. data/lib/workato/connector/sdk/lookup_tables.rb +62 -0
  27. data/lib/workato/connector/sdk/object_definitions.rb +74 -0
  28. data/lib/workato/connector/sdk/operation.rb +217 -0
  29. data/lib/workato/connector/sdk/request.rb +399 -0
  30. data/lib/workato/connector/sdk/settings.rb +130 -0
  31. data/lib/workato/connector/sdk/summarize.rb +61 -0
  32. data/lib/workato/connector/sdk/trigger.rb +96 -0
  33. data/lib/workato/connector/sdk/version.rb +9 -0
  34. data/lib/workato/connector/sdk/workato_schemas.rb +37 -0
  35. data/lib/workato/connector/sdk/xml.rb +35 -0
  36. data/lib/workato/connector/sdk.rb +58 -0
  37. data/lib/workato/extension/array.rb +124 -0
  38. data/lib/workato/extension/case_sensitive_headers.rb +51 -0
  39. data/lib/workato/extension/currency.rb +15 -0
  40. data/lib/workato/extension/date.rb +14 -0
  41. data/lib/workato/extension/enumerable.rb +55 -0
  42. data/lib/workato/extension/extra_chain_cert.rb +40 -0
  43. data/lib/workato/extension/hash.rb +13 -0
  44. data/lib/workato/extension/integer.rb +17 -0
  45. data/lib/workato/extension/nil_class.rb +17 -0
  46. data/lib/workato/extension/object.rb +38 -0
  47. data/lib/workato/extension/phone.rb +14 -0
  48. data/lib/workato/extension/string.rb +268 -0
  49. data/lib/workato/extension/symbol.rb +13 -0
  50. data/lib/workato/extension/time.rb +13 -0
  51. data/lib/workato/testing/vcr_encrypted_cassette_serializer.rb +38 -0
  52. data/lib/workato/testing/vcr_multipart_body_matcher.rb +32 -0
  53. data/lib/workato-connector-sdk.rb +3 -0
  54. data/templates/.rspec.erb +3 -0
  55. data/templates/Gemfile.erb +10 -0
  56. data/templates/connector.rb.erb +37 -0
  57. data/templates/spec/action_spec.rb.erb +36 -0
  58. data/templates/spec/connector_spec.rb.erb +18 -0
  59. data/templates/spec/method_spec.rb.erb +13 -0
  60. data/templates/spec/object_definition_spec.rb.erb +18 -0
  61. data/templates/spec/pick_list_spec.rb.erb +13 -0
  62. data/templates/spec/spec_helper.rb.erb +38 -0
  63. data/templates/spec/trigger_spec.rb.erb +61 -0
  64. metadata +372 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module CLI
5
+ module Generators
6
+ class MasterKeyGenerator < Thor::Group
7
+ include Thor::Actions
8
+
9
+ no_commands do
10
+ def call(key_path = Workato::Connector::Sdk::DEFAULT_MASTER_KEY_PATH)
11
+ create_key_file(key_path)
12
+ ignore_key_file(key_path)
13
+ end
14
+ end
15
+
16
+ def create_key_file(key_path = Workato::Connector::Sdk::DEFAULT_MASTER_KEY_PATH)
17
+ raise "#{key_path} already exists" if File.exist?(key_path)
18
+
19
+ key = ActiveSupport::EncryptedFile.generate_key
20
+
21
+ say "Adding #{key_path} to store the encryption key: #{key}"
22
+ say ''
23
+ say 'Save this in a password manager your team can access.'
24
+ say "Don't store the file in a public place, make sure you're sharing it privately."
25
+ say ''
26
+ say 'If you lose the key, no one, including you, can access anything encrypted with it.'
27
+
28
+ say ''
29
+
30
+ File.open(key_path, 'w') do |f|
31
+ f.write(key)
32
+ f.chmod 0o600
33
+ end
34
+
35
+ say ''
36
+ end
37
+
38
+ def ignore_key_file(key_path = Workato::Connector::Sdk::DEFAULT_MASTER_KEY_PATH)
39
+ ignore = [' ', "/#{key_path}", ' '].join("\n")
40
+ if File.exist?('.gitignore')
41
+ unless File.read('.gitignore').include?(ignore)
42
+ say "Ignoring #{key_path} so it won't end up in Git history:"
43
+ say ''
44
+ append_to_file '.gitignore', ignore
45
+ say ''
46
+ end
47
+ else
48
+ say "IMPORTANT: Don't commit #{key_path}. Add this to your ignore file:"
49
+ say ignore, :on_green
50
+ say ''
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'workato/connector/sdk'
5
+ require_relative './exec_command'
6
+ require_relative './edit_command'
7
+ require_relative './generate_command'
8
+ require_relative './push_command'
9
+ require_relative './generators/connector_generator'
10
+ require_relative './generators/master_key_generator'
11
+
12
+ module Workato
13
+ module CLI
14
+ class Main < Thor
15
+ class_option :verbose, type: :boolean
16
+
17
+ desc 'exec <PATH>', 'Execute connector defined block'
18
+ long_desc <<~HELP
19
+ The 'workato exec' executes connector's lambda block at <PATH>.
20
+ Lambda's parameters can be provided if needed, see options part.
21
+
22
+ Example:
23
+
24
+ workato exec actions.foo.execute # This executes execute block of foo action
25
+
26
+ workato exec triggers.bar.poll # This executes poll block of bar action
27
+
28
+ workato exec methods.bazz --args=input.json # This executes methods with params from input.json
29
+ HELP
30
+
31
+ method_option :connector, type: :string, aliases: '-c', desc: 'Path to connector source code',
32
+ lazy_default: Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
33
+ method_option :settings, type: :string, aliases: '-s',
34
+ desc: 'Path to plain or encrypted file with connection configs, '\
35
+ 'passwords, tokens, secrets etc',
36
+ lazy_default: Workato::Connector::Sdk::DEFAULT_ENCRYPTED_SETTINGS_PATH
37
+ method_option :connection, type: :string, aliases: '-n',
38
+ desc: 'Connection name if settings file contains multiple settings'
39
+ method_option :key, type: :string, aliases: '-k',
40
+ lazy_default: Workato::Connector::Sdk::DEFAULT_MASTER_KEY_PATH,
41
+ desc: 'Path to file with encrypt/decrypt key. '\
42
+ "NOTE: key from #{Workato::Connector::Sdk::DEFAULT_MASTER_KEY_ENV} has higher priority"
43
+ method_option :input, type: :string, aliases: '-i', desc: 'Path to file with input JSON'
44
+ method_option :closure, type: :string, desc: 'Path to file with next poll closure JSON'
45
+ method_option :args, type: :string, aliases: '-a', desc: 'Path to file with method arguments JSON'
46
+ method_option :extended_input_schema, type: :string,
47
+ desc: 'Path to file with extended input schema definition JSON'
48
+ method_option :extended_output_schema, type: :string,
49
+ desc: 'Path to file with extended output schema definition JSON'
50
+ method_option :config_fields, type: :string, desc: 'Path to file with config fields JSON'
51
+ method_option :webhook_payload, type: :string, aliases: '-w', desc: 'Path to file with webhook payload JSON'
52
+ method_option :webhook_params, type: :string, desc: 'Path to file with webhook params JSON'
53
+ method_option :webhook_headers, type: :string, desc: 'Path to file with webhook headers JSON'
54
+ method_option :webhook_url, type: :string, desc: 'Webhook URL for automatic webhook subscription'
55
+ method_option :output, type: :string, aliases: '-o', desc: 'Write output to JSON file'
56
+
57
+ method_option :debug, type: :boolean
58
+
59
+ def exec(path)
60
+ ExecCommand.new(
61
+ path: path,
62
+ options: options
63
+ ).call
64
+ end
65
+
66
+ desc 'edit <PATH>', 'Edit encrypted file, e.g. settings.yaml.enc'
67
+
68
+ method_option :key, type: :string, aliases: '-k',
69
+ lazy_default: Workato::Connector::Sdk::DEFAULT_MASTER_KEY_PATH,
70
+ desc: 'Path to file with encrypt/decrypt key. '\
71
+ "NOTE: key from #{Workato::Connector::Sdk::DEFAULT_MASTER_KEY_ENV} has higher priority"
72
+
73
+ def edit(path)
74
+ EditCommand.new(
75
+ path: path,
76
+ options: options
77
+ ).call
78
+ end
79
+
80
+ def self.exit_on_failure?
81
+ true
82
+ end
83
+
84
+ long_desc <<~HELP
85
+ The 'workato new' command creates a new Workato connector with a default
86
+ directory structure and configuration at the path you specify.
87
+
88
+ Example:
89
+ workato new ~/dev/workato/random
90
+
91
+ This generates a skeletal custom connector in ~/dev/workato/random.
92
+ HELP
93
+ register(Generators::ConnectorGenerator, 'new', 'new <CONNECTOR_PATH>', 'Inits new connector folder')
94
+
95
+ desc 'generate <SUBCOMMAND>', 'Generates code from template'
96
+ subcommand('generate', GenerateCommand)
97
+
98
+ desc 'push <FOLDER>', "Upload and release connector's code"
99
+ method_option :title,
100
+ type: :string,
101
+ aliases: '-t',
102
+ desc: 'Connector title on the Workato Platform'
103
+ method_option :description,
104
+ type: :string,
105
+ aliases: '-d',
106
+ desc: 'Path to connector description: Markdown or plain text'
107
+ method_option :logo,
108
+ type: :string,
109
+ aliases: '-l',
110
+ desc: 'Path to connector logo: png or jpeg file'
111
+ method_option :notes,
112
+ type: :string,
113
+ aliases: '-n',
114
+ desc: 'Release notes'
115
+ method_option :connector,
116
+ type: :string,
117
+ aliases: '-c',
118
+ desc: 'Path to connector source code',
119
+ lazy_default: Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
120
+ method_option :api_email,
121
+ type: :string,
122
+ desc: 'Email for accessing Workato API or '\
123
+ "set #{Workato::CLI::PushCommand::WORKATO_API_EMAIL_ENV} env"
124
+ method_option :api_token,
125
+ type: :string,
126
+ desc: 'Token for accessing Workato API or ' \
127
+ "set #{Workato::CLI::PushCommand::WORKATO_API_TOKEN_ENV} env"
128
+ method_option :environment,
129
+ type: :string,
130
+ enum: Workato::CLI::PushCommand::ENVIRONMENTS.keys,
131
+ default: 'live',
132
+ desc: 'Server to push connector code to'
133
+
134
+ def push(folder)
135
+ PushCommand.new(
136
+ folder: folder,
137
+ options: options
138
+ ).call
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'ruby-progressbar'
5
+ require 'zip'
6
+
7
+ module Workato
8
+ module CLI
9
+ class PushCommand
10
+ include Thor::Shell
11
+
12
+ WORKATO_API_TOKEN_ENV = 'WORKATO_API_TOKEN'
13
+ WORKATO_API_EMAIL_ENV = 'WORKATO_API_EMAIL'
14
+
15
+ ENVIRONMENTS = {
16
+ 'preview' => 'https://app.preview.workato.com',
17
+ 'preview-eu' => 'https://app.preview.eu.workato.com',
18
+ 'live' => 'https://app.workato.com',
19
+ 'live-eu' => 'https://app.eu.workato.com'
20
+ }.freeze
21
+
22
+ API_IMPORT_PATH = '/api/packages/import'
23
+ API_PACKAGE_PATH = '/api/packages'
24
+ IMPORT_IN_PROGRESS = 'in_progress'
25
+
26
+ DEFAULT_LOGO_PATH = 'logo.png'
27
+ DEFAULT_README_PATH = 'README.md'
28
+ PACKAGE_ENTRY_NAME = 'connector.custom_adapter'
29
+
30
+ AWAIT_IMPORT_SLEEP_INTERVAL = 15 # seconds
31
+ AWAIT_IMPORT_TIMEOUT_INTERVAL = 120 # seconds
32
+
33
+ def initialize(folder:, options:)
34
+ @folder_id = folder
35
+ @options = options
36
+ @api_base_url = ENVIRONMENTS.fetch(options[:environment])
37
+ @api_email = options[:api_email] || ENV[WORKATO_API_EMAIL_ENV]
38
+ @api_token = options[:api_token] || ENV[WORKATO_API_TOKEN_ENV]
39
+ end
40
+
41
+ def call
42
+ zip_file = build_package
43
+ say_status :success, 'Build package' if verbose?
44
+
45
+ import_id = import_package(zip_file)
46
+ say_status :success, 'Upload package' if verbose?
47
+ say_status :waiting, 'Process package' if verbose?
48
+
49
+ result = await_import(import_id)
50
+ if result.fetch('status') == 'failed'
51
+ say result.fetch('error').gsub("#{PACKAGE_ENTRY_NAME}.json: ", '')
52
+ else
53
+ say "Connector was successfully uploaded to #{api_base_url}"
54
+ end
55
+ rescue StandardError => e
56
+ say e.message
57
+ ensure
58
+ zip_file&.close(true)
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :options,
64
+ :folder_id,
65
+ :api_token,
66
+ :api_email,
67
+ :api_base_url
68
+
69
+ def verbose?
70
+ @options[:verbose]
71
+ end
72
+
73
+ def notes
74
+ options[:notes].presence || loop do
75
+ answer = ask('Please add release notes:')
76
+ break answer if answer.present?
77
+ end
78
+ end
79
+
80
+ def build_package
81
+ zip_file = Tempfile.new(['connector', '.zip'])
82
+
83
+ ::Zip::OutputStream.open(zip_file.path) { |_| 'no-op' }
84
+ ::Zip::File.open(zip_file.path, ::Zip::File::CREATE) do |archive|
85
+ add_connector(archive)
86
+ add_manifest(archive)
87
+ add_logo(archive)
88
+ end
89
+
90
+ zip_file
91
+ end
92
+
93
+ def add_connector(archive)
94
+ archive.get_output_stream("#{PACKAGE_ENTRY_NAME}.rb") do |f|
95
+ f.write(File.read(options[:connector] || Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH))
96
+ end
97
+ end
98
+
99
+ def add_manifest(archive)
100
+ archive.get_output_stream("#{PACKAGE_ENTRY_NAME}.json") do |f|
101
+ f.write(JSON.pretty_generate(metadata))
102
+ end
103
+ end
104
+
105
+ def add_logo(archive)
106
+ return unless logo
107
+
108
+ archive.get_output_stream("#{PACKAGE_ENTRY_NAME}#{logo[:extname]}") do |f|
109
+ f.write(logo[:content])
110
+ end
111
+ end
112
+
113
+ def import_package(zip_file)
114
+ url = "#{api_base_url}#{API_IMPORT_PATH}/#{folder_id}"
115
+ response = RestClient.post(
116
+ url,
117
+ File.open(zip_file.path),
118
+ auth_headers.merge(
119
+ 'Content-Type' => 'application/zip'
120
+ )
121
+ )
122
+ JSON.parse(response.body).fetch('id')
123
+ rescue RestClient::NotFound
124
+ raise "Can't find folder with ID=#{folder_id}"
125
+ rescue RestClient::BadRequest => e
126
+ message = JSON.parse(e.response.body).fetch('error')
127
+ raise "Failed to upload connector: #{message}"
128
+ end
129
+
130
+ def await_import(import_id)
131
+ url = "#{api_base_url}#{API_PACKAGE_PATH}/#{import_id}"
132
+ Timeout.timeout(AWAIT_IMPORT_TIMEOUT_INTERVAL) do
133
+ loop do
134
+ response = RestClient.get(url, auth_headers)
135
+
136
+ json = JSON.parse(response.body)
137
+ break json if json.fetch('status') != IMPORT_IN_PROGRESS
138
+
139
+ sleep(AWAIT_IMPORT_SLEEP_INTERVAL)
140
+ end
141
+ end
142
+ rescue Timeout::Error
143
+ raise 'Failed to wait import result. Go to Imports in Workato UI to see the push result'
144
+ end
145
+
146
+ def logo
147
+ return @logo if defined?(@logo)
148
+
149
+ path = (options.key?(:logo) && options[:logo]) ||
150
+ (File.exist?(DEFAULT_LOGO_PATH) && DEFAULT_LOGO_PATH)
151
+ return @logo = nil unless path
152
+
153
+ extname = File.extname(path).downcase
154
+ @logo = {
155
+ extname: extname,
156
+ content: File.read(path),
157
+ content_type: extname == '.png' ? 'image/png' : 'image/jpeg'
158
+ }
159
+ end
160
+
161
+ def metadata
162
+ {
163
+ title: title,
164
+ description: description,
165
+ note: notes
166
+ }.tap do |meta|
167
+ if logo
168
+ meta[:logo_file_name] = 'data'
169
+ meta[:logo_content_type] = logo[:content_type]
170
+ end
171
+ end
172
+ end
173
+
174
+ def title
175
+ options[:title].presence || connector.title.presence || loop do
176
+ answer = ask('Please provide title of the connector:')
177
+ break answer if answer.present?
178
+ end
179
+ end
180
+
181
+ def description
182
+ (options[:description].presence && File.read(options[:description])) ||
183
+ (File.exist?(DEFAULT_README_PATH) && File.read(DEFAULT_README_PATH)) ||
184
+ nil
185
+ end
186
+
187
+ def connector
188
+ @connector ||= Workato::Connector::Sdk::Connector.from_file(
189
+ options[:connector] || Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
190
+ )
191
+ end
192
+
193
+ def auth_headers
194
+ {
195
+ 'x-user-email' => api_email,
196
+ 'x-user-token' => api_token
197
+ }
198
+ end
199
+
200
+ private_constant :IMPORT_IN_PROGRESS,
201
+ :API_IMPORT_PATH,
202
+ :API_PACKAGE_PATH,
203
+ :PACKAGE_ENTRY_NAME,
204
+ :AWAIT_IMPORT_SLEEP_INTERVAL,
205
+ :AWAIT_IMPORT_TIMEOUT_INTERVAL
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require 'erb'
5
+ require 'singleton'
6
+
7
+ module Workato
8
+ module Connector
9
+ module Sdk
10
+ class AccountProperties
11
+ include Singleton
12
+
13
+ def self.from_yaml(path = DEFAULT_ACCOUNT_PROPERTIES_PATH)
14
+ File.open(path) do |f|
15
+ instance.load_data(YAML.safe_load(ERB.new(f.read).result, [::Symbol]).to_hash)
16
+ end
17
+ end
18
+
19
+ def self.from_encrypted_yaml(path = DEFAULT_ENCRYPTED_ACCOUNT_PROPERTIES_PATH, key_path = nil)
20
+ load_data(
21
+ ActiveSupport::EncryptedConfiguration.new(
22
+ config_path: path,
23
+ key_path: key_path || DEFAULT_MASTER_KEY_PATH,
24
+ env_key: DEFAULT_MASTER_KEY_ENV,
25
+ raise_if_missing_key: true
26
+ ).config
27
+ )
28
+ end
29
+
30
+ def self.from_csv(path = './account_properties.csv')
31
+ props = CSV.foreach(path, headers: true, return_headers: false).map do |row|
32
+ [row[0], row[1]]
33
+ end.to_h
34
+ instance.load_data(props)
35
+ end
36
+
37
+ class << self
38
+ delegate :load_data,
39
+ :get,
40
+ :put,
41
+ to: :instance
42
+ end
43
+
44
+ def get(key)
45
+ @data ||= {}
46
+ @data[key.to_s]
47
+ end
48
+
49
+ def put(key, value)
50
+ @data ||= {}
51
+ @data[key.to_s] = value.to_s
52
+ end
53
+
54
+ def load_data(props = {})
55
+ props.each { |k, v| put(k, v) }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end