workato-connector-sdk 0.1.0

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.
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