etl-integrations 0.1.81

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +34 -0
  4. data/.ruby-version +1 -0
  5. data/.vscode/settings.json +5 -0
  6. data/CHANGELOG.md +38 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +105 -0
  10. data/Rakefile +12 -0
  11. data/lib/multiwoven/integrations/config.rb +13 -0
  12. data/lib/multiwoven/integrations/core/base_connector.rb +70 -0
  13. data/lib/multiwoven/integrations/core/constants.rb +46 -0
  14. data/lib/multiwoven/integrations/core/destination_connector.rb +14 -0
  15. data/lib/multiwoven/integrations/core/fullrefresher.rb +19 -0
  16. data/lib/multiwoven/integrations/core/http_client.rb +34 -0
  17. data/lib/multiwoven/integrations/core/query_builder.rb +27 -0
  18. data/lib/multiwoven/integrations/core/rate_limiter.rb +19 -0
  19. data/lib/multiwoven/integrations/core/source_connector.rb +38 -0
  20. data/lib/multiwoven/integrations/core/utils.rb +104 -0
  21. data/lib/multiwoven/integrations/destination/airtable/client.rb +153 -0
  22. data/lib/multiwoven/integrations/destination/airtable/config/catalog.json +6 -0
  23. data/lib/multiwoven/integrations/destination/airtable/config/meta.json +15 -0
  24. data/lib/multiwoven/integrations/destination/airtable/config/spec.json +22 -0
  25. data/lib/multiwoven/integrations/destination/airtable/icon.svg +6 -0
  26. data/lib/multiwoven/integrations/destination/airtable/schema_helper.rb +141 -0
  27. data/lib/multiwoven/integrations/destination/facebook_custom_audience/client.rb +124 -0
  28. data/lib/multiwoven/integrations/destination/facebook_custom_audience/config/catalog.json +42 -0
  29. data/lib/multiwoven/integrations/destination/facebook_custom_audience/config/meta.json +15 -0
  30. data/lib/multiwoven/integrations/destination/facebook_custom_audience/config/spec.json +27 -0
  31. data/lib/multiwoven/integrations/destination/facebook_custom_audience/icon.svg +23 -0
  32. data/lib/multiwoven/integrations/destination/google_sheets/client.rb +231 -0
  33. data/lib/multiwoven/integrations/destination/google_sheets/config/catalog.json +6 -0
  34. data/lib/multiwoven/integrations/destination/google_sheets/config/meta.json +15 -0
  35. data/lib/multiwoven/integrations/destination/google_sheets/config/spec.json +74 -0
  36. data/lib/multiwoven/integrations/destination/google_sheets/icon.svg +1 -0
  37. data/lib/multiwoven/integrations/destination/hubspot/client.rb +107 -0
  38. data/lib/multiwoven/integrations/destination/hubspot/config/catalog.json +351 -0
  39. data/lib/multiwoven/integrations/destination/hubspot/config/meta.json +15 -0
  40. data/lib/multiwoven/integrations/destination/hubspot/config/spec.json +17 -0
  41. data/lib/multiwoven/integrations/destination/hubspot/icon.svg +5 -0
  42. data/lib/multiwoven/integrations/destination/klaviyo/client.rb +116 -0
  43. data/lib/multiwoven/integrations/destination/klaviyo/config/catalog.json +103 -0
  44. data/lib/multiwoven/integrations/destination/klaviyo/config/meta.json +15 -0
  45. data/lib/multiwoven/integrations/destination/klaviyo/config/spec.json +22 -0
  46. data/lib/multiwoven/integrations/destination/klaviyo/icon.svg +6 -0
  47. data/lib/multiwoven/integrations/destination/postgresql/client.rb +123 -0
  48. data/lib/multiwoven/integrations/destination/postgresql/config/meta.json +15 -0
  49. data/lib/multiwoven/integrations/destination/postgresql/config/spec.json +68 -0
  50. data/lib/multiwoven/integrations/destination/postgresql/icon.svg +20 -0
  51. data/lib/multiwoven/integrations/destination/salesforce_consumer_goods_cloud/client.rb +114 -0
  52. data/lib/multiwoven/integrations/destination/salesforce_consumer_goods_cloud/config/catalog.json +6 -0
  53. data/lib/multiwoven/integrations/destination/salesforce_consumer_goods_cloud/config/meta.json +16 -0
  54. data/lib/multiwoven/integrations/destination/salesforce_consumer_goods_cloud/config/spec.json +49 -0
  55. data/lib/multiwoven/integrations/destination/salesforce_consumer_goods_cloud/icon.svg +16 -0
  56. data/lib/multiwoven/integrations/destination/salesforce_consumer_goods_cloud/schema_helper.rb +132 -0
  57. data/lib/multiwoven/integrations/destination/salesforce_crm/client.rb +117 -0
  58. data/lib/multiwoven/integrations/destination/salesforce_crm/config/catalog.json +320 -0
  59. data/lib/multiwoven/integrations/destination/salesforce_crm/config/meta.json +15 -0
  60. data/lib/multiwoven/integrations/destination/salesforce_crm/config/spec.json +43 -0
  61. data/lib/multiwoven/integrations/destination/salesforce_crm/icon.svg +16 -0
  62. data/lib/multiwoven/integrations/destination/sftp/client.rb +133 -0
  63. data/lib/multiwoven/integrations/destination/sftp/config/catalog.json +16 -0
  64. data/lib/multiwoven/integrations/destination/sftp/config/meta.json +16 -0
  65. data/lib/multiwoven/integrations/destination/sftp/config/spec.json +50 -0
  66. data/lib/multiwoven/integrations/destination/sftp/icon.svg +1 -0
  67. data/lib/multiwoven/integrations/destination/slack/client.rb +114 -0
  68. data/lib/multiwoven/integrations/destination/slack/config/catalog.json +22 -0
  69. data/lib/multiwoven/integrations/destination/slack/config/meta.json +15 -0
  70. data/lib/multiwoven/integrations/destination/slack/config/spec.json +22 -0
  71. data/lib/multiwoven/integrations/destination/slack/icon.svg +26 -0
  72. data/lib/multiwoven/integrations/destination/stripe/client.rb +82 -0
  73. data/lib/multiwoven/integrations/destination/stripe/config/catalog.json +128 -0
  74. data/lib/multiwoven/integrations/destination/stripe/config/meta.json +15 -0
  75. data/lib/multiwoven/integrations/destination/stripe/config/spec.json +17 -0
  76. data/lib/multiwoven/integrations/destination/stripe/icon.svg +10 -0
  77. data/lib/multiwoven/integrations/destination/tally/client.rb +151 -0
  78. data/lib/multiwoven/integrations/destination/tally/config/catalog.json +62 -0
  79. data/lib/multiwoven/integrations/destination/tally/config/meta.json +15 -0
  80. data/lib/multiwoven/integrations/destination/tally/config/spec.json +45 -0
  81. data/lib/multiwoven/integrations/destination/tally/icon.svg +7 -0
  82. data/lib/multiwoven/integrations/protocol/protocol.json +189 -0
  83. data/lib/multiwoven/integrations/protocol/protocol.rb +216 -0
  84. data/lib/multiwoven/integrations/rollout.rb +32 -0
  85. data/lib/multiwoven/integrations/service.rb +79 -0
  86. data/lib/multiwoven/integrations/source/bigquery/client.rb +98 -0
  87. data/lib/multiwoven/integrations/source/bigquery/config/meta.json +15 -0
  88. data/lib/multiwoven/integrations/source/bigquery/config/spec.json +82 -0
  89. data/lib/multiwoven/integrations/source/bigquery/icon.svg +1 -0
  90. data/lib/multiwoven/integrations/source/databricks/client.rb +98 -0
  91. data/lib/multiwoven/integrations/source/databricks/config/meta.json +16 -0
  92. data/lib/multiwoven/integrations/source/databricks/config/spec.json +56 -0
  93. data/lib/multiwoven/integrations/source/databricks/icon.svg +19 -0
  94. data/lib/multiwoven/integrations/source/postgresql/client.rb +109 -0
  95. data/lib/multiwoven/integrations/source/postgresql/config/meta.json +15 -0
  96. data/lib/multiwoven/integrations/source/postgresql/config/spec.json +69 -0
  97. data/lib/multiwoven/integrations/source/postgresql/icon.svg +20 -0
  98. data/lib/multiwoven/integrations/source/redshift/client.rb +109 -0
  99. data/lib/multiwoven/integrations/source/redshift/config/meta.json +15 -0
  100. data/lib/multiwoven/integrations/source/redshift/config/spec.json +71 -0
  101. data/lib/multiwoven/integrations/source/redshift/icon.svg +15 -0
  102. data/lib/multiwoven/integrations/source/salesforce_consumer_goods_cloud/client.rb +123 -0
  103. data/lib/multiwoven/integrations/source/salesforce_consumer_goods_cloud/config/catalog.json +6 -0
  104. data/lib/multiwoven/integrations/source/salesforce_consumer_goods_cloud/config/meta.json +17 -0
  105. data/lib/multiwoven/integrations/source/salesforce_consumer_goods_cloud/config/spec.json +50 -0
  106. data/lib/multiwoven/integrations/source/salesforce_consumer_goods_cloud/icon.svg +16 -0
  107. data/lib/multiwoven/integrations/source/salesforce_consumer_goods_cloud/schema_helper.rb +130 -0
  108. data/lib/multiwoven/integrations/source/snowflake/client.rb +92 -0
  109. data/lib/multiwoven/integrations/source/snowflake/config/meta.json +15 -0
  110. data/lib/multiwoven/integrations/source/snowflake/config/spec.json +82 -0
  111. data/lib/multiwoven/integrations/source/snowflake/icon.svg +10 -0
  112. data/lib/multiwoven/integrations/source/zoho_books/client.rb +155 -0
  113. data/lib/multiwoven/integrations/source/zoho_books/config/meta.json +16 -0
  114. data/lib/multiwoven/integrations/source/zoho_books/config/spec.json +43 -0
  115. data/lib/multiwoven/integrations/source/zoho_books/icon.svg +16 -0
  116. data/lib/multiwoven/integrations.rb +71 -0
  117. data/multiwoven-integrations-0.1.68.gem +0 -0
  118. data/sig/multiwoven/integrations.rbs +6 -0
  119. metadata +515 -0
@@ -0,0 +1,23 @@
1
+ <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1365.3333 1365.3333" height="1365.3333" width="1365.3333" xml:space="preserve" id="svg2" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
2
+ <metadata id="metadata8">
3
+ <rdf:RDF>
4
+ <cc:Work rdf:about="">
5
+ <dc:format>
6
+ image/svg+xml
7
+ </dc:format>
8
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage">
9
+ </dc:type>
10
+ </cc:Work>
11
+ </rdf:RDF>
12
+ </metadata>
13
+ <defs id="defs6">
14
+ </defs>
15
+ <g transform="matrix(1.3333333,0,0,-1.3333333,0,1365.3333)" id="g10">
16
+ <g transform="scale(0.1)" id="g12">
17
+ <path id="path14" style="fill:#1877f2;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 10240,5120 c 0,2827.7 -2292.3,5120 -5120,5120 C 2292.3,10240 0,7947.7 0,5120 0,2564.46 1872.31,446.301 4320,62.1992 V 3640 H 3020 v 1480 h 1300 v 1128 c 0,1283.2 764.38,1992 1933.9,1992 560.17,0 1146.1,-100 1146.1,-100 V 6880 H 6754.38 C 6118.35,6880 5920,6485.33 5920,6080.43 V 5120 H 7340 L 7113,3640 H 5920 V 62.1992 C 8367.69,446.301 10240,2564.46 10240,5120">
18
+ </path>
19
+ <path id="path16" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 7113,3640 227,1480 H 5920 v 960.43 c 0,404.9 198.35,799.57 834.38,799.57 H 7400 v 1260 c 0,0 -585.93,100 -1146.1,100 C 5084.38,8240 4320,7531.2 4320,6248 V 5120 H 3020 V 3640 H 4320 V 62.1992 C 4580.67,21.3008 4847.84,0 5120,0 c 272.16,0 539.33,21.3008 800,62.1992 V 3640 h 1193">
20
+ </path>
21
+ </g>
22
+ </g>
23
+ </svg>
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multiwoven
4
+ module Integrations
5
+ module Destination
6
+ module GoogleSheets
7
+ include Multiwoven::Integrations::Core
8
+
9
+ class Client < DestinationConnector # rubocop:disable Metrics/ClassLength
10
+ prepend Multiwoven::Integrations::Core::Fullrefresher
11
+ prepend Multiwoven::Integrations::Core::RateLimiter
12
+ MAX_CHUNK_SIZE = 10_000
13
+
14
+ def check_connection(connection_config)
15
+ connection_config = connection_config.with_indifferent_access
16
+ authorize_client(connection_config)
17
+ fetch_google_spread_sheets(connection_config)
18
+ success_status
19
+ rescue StandardError => e
20
+ handle_exception("GOOGLE_SHEETS:CRM:DISCOVER:EXCEPTION", "error", e)
21
+ failure_status(e)
22
+ end
23
+
24
+ def discover(connection_config)
25
+ connection_config = connection_config.with_indifferent_access
26
+ authorize_client(connection_config)
27
+ spreadsheets = fetch_google_spread_sheets(connection_config)
28
+ catalog = build_catalog_from_spreadsheets(spreadsheets, connection_config)
29
+ catalog.to_multiwoven_message
30
+ rescue StandardError => e
31
+ handle_exception("GOOGLE_SHEETS:CRM:DISCOVER:EXCEPTION", "error", e)
32
+ end
33
+
34
+ def write(sync_config, records, action = "create")
35
+ setup_write_environment(sync_config, action)
36
+ process_record_chunks(records, sync_config)
37
+ rescue StandardError => e
38
+ handle_exception("GOOGLE_SHEETS:CRM:WRITE:EXCEPTION", "error", e)
39
+ end
40
+
41
+ def clear_all_records(sync_config)
42
+ setup_write_environment(sync_config, "clear")
43
+ connection_specification = sync_config.destination.connection_specification.with_indifferent_access
44
+ spreadsheet = fetch_google_spread_sheets(connection_specification)
45
+ sheet_ids = spreadsheet.sheets.map(&:properties).map(&:sheet_id)
46
+
47
+ delete_extra_sheets(sheet_ids)
48
+
49
+ unless sheet_ids.empty?
50
+ clear_response = clear_sheet_data(spreadsheet.sheets.first.properties.title)
51
+ return control_message("Successfully cleared data.", "succeeded") if clear_response&.cleared_range
52
+ end
53
+
54
+ control_message("Failed to clear data.", "failed")
55
+ rescue StandardError => e
56
+ control_message(e.message, "failed")
57
+ end
58
+
59
+ private
60
+
61
+ # To define the level of access granted to your app, you need to identify and declare authorization scopes which is provided by google scopse https://developers.google.com/sheets/api/scopes
62
+ def authorize_client(config)
63
+ credentials = config[:credentials_json]
64
+ @client = Google::Apis::SheetsV4::SheetsService.new
65
+ @client.authorization = Google::Auth::ServiceAccountCredentials.make_creds(
66
+ json_key_io: StringIO.new(credentials.to_json),
67
+ scope: GOOGLE_SHEETS_SCOPE
68
+ )
69
+ end
70
+
71
+ # Extract spreadsheet id from the spreadsheet link and return the metadata for all the sheets
72
+ def fetch_google_spread_sheets(connection_config)
73
+ spreadsheet_id = extract_spreadsheet_id(connection_config[:spreadsheet_link])
74
+ @client.get_spreadsheet(spreadsheet_id)
75
+ end
76
+
77
+ # dynamically builds catalog based on spreadsheet metadata
78
+ def build_catalog_from_spreadsheets(spreadsheet, connection_config)
79
+ catalog = build_catalog(load_catalog)
80
+ @spreadsheet_id = extract_spreadsheet_id(connection_config[:spreadsheet_link])
81
+
82
+ spreadsheet.sheets.each do |sheet|
83
+ process_sheet_for_catalog(sheet, catalog)
84
+ end
85
+
86
+ catalog
87
+ end
88
+
89
+ # Builds catalog for the single spreadsheet based on column name
90
+ def process_sheet_for_catalog(sheet, catalog)
91
+ sheet_name, last_column_index = extract_sheet_properties(sheet)
92
+ column_names = fetch_column_names(sheet_name, last_column_index)
93
+ catalog.streams << generate_json_schema(column_names, sheet_name) if column_names
94
+ end
95
+
96
+ def extract_sheet_properties(sheet)
97
+ [sheet.properties.title, sheet.properties.grid_properties.column_count]
98
+ end
99
+
100
+ def fetch_column_names(sheet_name, last_column_index)
101
+ header_range = generate_header_range(sheet_name, last_column_index)
102
+ spread_sheet_value(header_range)&.flatten
103
+ end
104
+
105
+ def spread_sheet_value(header_range)
106
+ @spread_sheet_value ||= @client.get_spreadsheet_values(@spreadsheet_id, header_range).values
107
+ end
108
+
109
+ def generate_header_range(sheet_name, last_column_index)
110
+ "#{sheet_name}!A1:#{column_index_to_letter(last_column_index)}1"
111
+ end
112
+
113
+ def column_index_to_letter(index)
114
+ ("A".."ZZZ").to_a[index - 1]
115
+ end
116
+
117
+ def generate_json_schema(column_names, sheet_name)
118
+ {
119
+ name: sheet_name,
120
+ action: "create",
121
+ batch_support: true,
122
+ batch_size: 10_000,
123
+ json_schema: generate_properties_schema(column_names),
124
+ supported_sync_modes: %w[incremental full_refresh]
125
+ }.with_indifferent_access
126
+ end
127
+
128
+ def generate_properties_schema(column_names)
129
+ properties = column_names.each_with_object({}) do |field, props|
130
+ props[field] = { "type" => "string" }
131
+ end
132
+
133
+ { "$schema" => JSON_SCHEMA_URL, "type" => "object", "properties" => properties }
134
+ end
135
+
136
+ def setup_write_environment(sync_config, action)
137
+ @action = sync_config.stream.action || action
138
+ connection_specification = sync_config.destination.connection_specification.with_indifferent_access
139
+ @spreadsheet_id = extract_spreadsheet_id(connection_specification[:spreadsheet_link])
140
+ authorize_client(connection_specification)
141
+ end
142
+
143
+ def extract_spreadsheet_id(link)
144
+ link[GOOGLE_SPREADSHEET_ID_REGEX, 1] || link
145
+ end
146
+
147
+ # Batch has a limit of sending 2MB data. So creating a chunk of records to meet that limit
148
+ def process_record_chunks(records, sync_config)
149
+ write_success = 0
150
+ write_failure = 0
151
+
152
+ records.each_slice(MAX_CHUNK_SIZE) do |chunk|
153
+ values = prepare_chunk_values(chunk, sync_config.stream)
154
+ update_sheet_values(values, sync_config.stream.name)
155
+ write_success += values.size
156
+ rescue StandardError => e
157
+ handle_exception("GOOGLE_SHEETS:RECORD:WRITE:EXCEPTION", "error", e)
158
+ write_failure += chunk.size
159
+ end
160
+
161
+ tracking_message(write_success, write_failure)
162
+ end
163
+
164
+ # We need to format the data to adhere to google sheets API format. This converts the sync mapped data to 2D array format expected by google sheets API
165
+ def prepare_chunk_values(chunk, stream)
166
+ last_column_index = spread_sheet_value(stream.name).count
167
+ fields = fetch_column_names(stream.name, last_column_index)
168
+
169
+ chunk.map do |row|
170
+ row_values = Array.new(fields.size, nil)
171
+ row.each do |key, value|
172
+ index = fields.index(key.to_s)
173
+ row_values[index] = value if index
174
+ end
175
+ row_values
176
+ end
177
+ end
178
+
179
+ def update_sheet_values(values, stream_name)
180
+ row_count = spread_sheet_value(stream_name).count
181
+ range = "#{stream_name}!A#{row_count + 1}"
182
+ value_range = Google::Apis::SheetsV4::ValueRange.new(range: range, values: values)
183
+
184
+ batch_update_request = Google::Apis::SheetsV4::BatchUpdateValuesRequest.new(
185
+ value_input_option: "RAW",
186
+ data: [value_range]
187
+ )
188
+
189
+ # TODO: Remove & this is added for the test to pass we need
190
+ @client&.batch_update_values(@spreadsheet_id, batch_update_request)
191
+ end
192
+
193
+ def load_catalog
194
+ read_json(CATALOG_SPEC_PATH)
195
+ end
196
+
197
+ def tracking_message(success, failure)
198
+ Multiwoven::Integrations::Protocol::TrackingMessage.new(
199
+ success: success, failed: failure
200
+ ).to_multiwoven_message
201
+ end
202
+
203
+ def delete_extra_sheets(sheet_ids)
204
+ # Leave one sheet intact as a spreadsheet must have at least one sheet.
205
+ # Delete all other sheets.
206
+ (sheet_ids.length - 1).times do |i|
207
+ request = Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new(
208
+ requests: [{ delete_sheet: { sheet_id: sheet_ids[i + 1] } }]
209
+ )
210
+ @client.batch_update_spreadsheet(@spreadsheet_id, request)
211
+ end
212
+ end
213
+
214
+ def clear_sheet_data(sheet_title)
215
+ clear_request = Google::Apis::SheetsV4::ClearValuesRequest.new
216
+ @client&.clear_values(@spreadsheet_id, "#{sheet_title}!A2:Z", clear_request)
217
+ end
218
+
219
+ def control_message(message, status)
220
+ ControlMessage.new(
221
+ type: "full_refresh",
222
+ emitted_at: Time.now.to_i,
223
+ status: ConnectionStatusType[status],
224
+ meta: { detail: message }
225
+ ).to_multiwoven_message
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,6 @@
1
+ {
2
+ "request_rate_limit": 300,
3
+ "request_rate_limit_unit": "minute",
4
+ "request_rate_concurrency": 10,
5
+ "streams": []
6
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "data": {
3
+ "name": "GoogleSheets",
4
+ "title": "Google Sheets",
5
+ "connector_type": "destination",
6
+ "category": "Productivity Tools",
7
+ "documentation_url": "https://docs.multiwoven.com/destinations/productivity-tools/google-sheets-service-account",
8
+ "github_issue_label": "destination-google-sheets",
9
+ "icon": "icon.svg",
10
+ "license": "MIT",
11
+ "release_stage": "alpha",
12
+ "support_level": "community",
13
+ "tags": ["language:ruby", "multiwoven"]
14
+ }
15
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "documentation_url": "https://docs.multiwoven.com/destinations/productivity-tools/google-sheets-service-account",
3
+ "stream_type": "dynamic",
4
+ "connection_specification": {
5
+ "$schema": "http://json-schema.org/draft-07/schema#",
6
+ "title": "Google Sheets",
7
+ "type": "object",
8
+ "required": ["credentials_json"],
9
+ "properties": {
10
+ "spreadsheet_link": {
11
+ "type": "string"
12
+ },
13
+ "credentials_json": {
14
+ "type": "object",
15
+ "description": "You can get the keys from the Google Cloud web console. First, go to the IAM page and select Service Accounts from the left menu. Next, locate your service account in the list, click on its Keys tab, and then click Add Key. Lastly, click Create new key and select JSON.",
16
+ "title": "",
17
+ "properties": {
18
+ "type": {
19
+ "type": "string",
20
+ "enum": ["service_account"]
21
+ },
22
+ "project_id": {
23
+ "type": "string"
24
+ },
25
+ "private_key_id": {
26
+ "type": "string"
27
+ },
28
+ "private_key": {
29
+ "type": "string"
30
+ },
31
+ "client_email": {
32
+ "type": "string",
33
+ "format": "email"
34
+ },
35
+ "client_id": {
36
+ "type": "string"
37
+ },
38
+ "auth_uri": {
39
+ "type": "string",
40
+ "format": "uri"
41
+ },
42
+ "token_uri": {
43
+ "type": "string",
44
+ "format": "uri"
45
+ },
46
+ "auth_provider_x509_cert_url": {
47
+ "type": "string",
48
+ "format": "uri"
49
+ },
50
+ "client_x509_cert_url": {
51
+ "type": "string",
52
+ "format": "uri"
53
+ },
54
+ "universe_domain": {
55
+ "type": "string"
56
+ }
57
+ },
58
+ "required": [
59
+ "type",
60
+ "project_id",
61
+ "private_key_id",
62
+ "private_key",
63
+ "client_email",
64
+ "client_id",
65
+ "auth_uri",
66
+ "token_uri",
67
+ "auth_provider_x509_cert_url",
68
+ "client_x509_cert_url",
69
+ "universe_domain"
70
+ ]
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 242423 333333" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"><defs><mask id="c"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="200294" y1="91174.8" x2="200294" y2="176113"><stop offset="0" stop-opacity=".02" stop-color="#fff"/><stop offset="1" stop-opacity=".2" stop-color="#fff"/></linearGradient><path fill="url(#a)" d="M158015 84111h84558v99065h-84558z"/></mask><mask id="e"><radialGradient id="b" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="0" fx="0" fy="0"><stop offset="0" stop-opacity="0" stop-color="#fff"/><stop offset="1" stop-opacity=".098" stop-color="#fff"/></radialGradient><path fill="url(#b)" d="M-150-150h242723v333633H-150z"/></mask><radialGradient id="f" gradientUnits="userSpaceOnUse" cx="9696.85" cy="10000.4" r="166667" fx="9696.85" fy="10000.4"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#fff"/></radialGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="200294" y1="95125.2" x2="200294" y2="172162"><stop offset="0" stop-color="#263138"/><stop offset="1" stop-color="#263138"/></linearGradient></defs><g fill-rule="nonzero"><path d="M151513 0H22729C10227 0 1 10227 1 22728v287877c0 12505 10227 22728 22728 22728h196966c12505 0 22728-10224 22728-22728V90911l-53028-37880L151513 0z" fill="#0f9c57"/><path d="M60606 162880v109853h121216V162880H60606zm53032 94698H75757v-18938h37881v18938zm0-30301H75757v-18946h37881v18946zm0-30310H75757v-18936h37881v18936zm53030 60611h-37884v-18938h37884v18938zm0-30301h-37884v-18946h37884v18946zm0-30310h-37884v-18936h37884v18936z" fill="#f0f0f0"/><path mask="url(#c)" fill="url(#d)" d="M158165 84261l84258 84245V90911z"/><path d="M151513 0v68184c0 12557 10173 22727 22727 22727h68183L151513 0z" fill="#87cdac"/><path d="M22728 0C10226 0 0 10227 0 22729v1893C0 12123 10227 1894 22728 1894h128784V1H22728z" fill="#fff" fill-opacity=".2"/><path d="M219694 331443H22728C10226 331443 0 321213 0 308715v1890c0 12505 10227 22728 22728 22728h196966c12505 0 22728-10224 22728-22728v-1890c0 12499-10224 22728-22728 22728z" fill="#263138" fill-opacity=".2"/><path d="M174239 90911c-12554 0-22727-10170-22727-22727v1893c0 12557 10173 22727 22727 22727h68183v-1893h-68183z" fill="#263138" fill-opacity=".102"/><path d="M151513 0H22729C10227 0 1 10227 1 22729v287876c0 12505 10227 22728 22728 22728h196966c12505 0 22728-10224 22728-22728V90911L151513 0z" mask="url(#e)" fill="url(#f)"/></g></svg>
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Multiwoven
6
+ module Integrations
7
+ module Destination
8
+ module Hubspot
9
+ include Multiwoven::Integrations::Core
10
+
11
+ class Client < DestinationConnector
12
+ prepend Multiwoven::Integrations::Core::RateLimiter
13
+ def check_connection(connection_config)
14
+ connection_config = connection_config.with_indifferent_access
15
+ initialize_client(connection_config)
16
+ authenticate_client
17
+ success_status
18
+ rescue StandardError => e
19
+ handle_exception("HUBSPOT:CRM:DISCOVER:EXCEPTION", "error", e)
20
+ failure_status(e)
21
+ end
22
+
23
+ def discover(_connection_config = nil)
24
+ catalog = build_catalog(load_catalog)
25
+ catalog.to_multiwoven_message
26
+ rescue StandardError => e
27
+ handle_exception("HUBSPOT:CRM:DISCOVER:EXCEPTION", "error", e)
28
+ end
29
+
30
+ def write(sync_config, records, action = "create")
31
+ @action = sync_config.stream.action || action
32
+ initialize_client(sync_config.destination.connection_specification)
33
+ process_records(records, sync_config.stream)
34
+ rescue StandardError => e
35
+ handle_exception("HUBSPOT:CRM:WRITE:EXCEPTION", "error", e)
36
+ end
37
+
38
+ private
39
+
40
+ def initialize_client(config)
41
+ config = config.with_indifferent_access
42
+ @client = ::Hubspot::Client.new(access_token: config[:access_token])
43
+ end
44
+
45
+ def process_records(records, stream)
46
+ write_success = 0
47
+ write_failure = 0
48
+ properties = stream.json_schema.with_indifferent_access[:properties]
49
+ records.each do |record_object|
50
+ record = extract_data(record_object, properties)
51
+ send_data_to_hubspot(stream.name, record)
52
+ write_success += 1
53
+ rescue StandardError => e
54
+ handle_exception("HUBSPOT:CRM:WRITE:EXCEPTION", "error", e)
55
+ write_failure += 1
56
+ end
57
+ tracking_message(write_success, write_failure)
58
+ end
59
+
60
+ def send_data_to_hubspot(stream_name, record = {})
61
+ args = build_args(@action, stream_name, record)
62
+ hubspot_stream = @client.crm.send(stream_name)
63
+ hubspot_data = { simple_public_object_input_for_create: args }
64
+ hubspot_stream.basic_api.send(@action, hubspot_data)
65
+ end
66
+
67
+ def build_args(action, stream_name, record)
68
+ case action
69
+ when :upsert
70
+ [stream_name, record[:external_key], record]
71
+ when :destroy
72
+ [stream_name, record[:id]]
73
+ else
74
+ record
75
+ end
76
+ end
77
+
78
+ def authenticate_client
79
+ @client.crm.contacts.basic_api.get_page
80
+ end
81
+
82
+ def success_status
83
+ ConnectionStatus.new(status: ConnectionStatusType["succeeded"]).to_multiwoven_message
84
+ end
85
+
86
+ def failure_status(error)
87
+ ConnectionStatus.new(status: ConnectionStatusType["failed"], message: error.message).to_multiwoven_message
88
+ end
89
+
90
+ def load_catalog
91
+ read_json(CATALOG_SPEC_PATH)
92
+ end
93
+
94
+ def tracking_message(success, failure)
95
+ Multiwoven::Integrations::Protocol::TrackingMessage.new(
96
+ success: success, failed: failure
97
+ ).to_multiwoven_message
98
+ end
99
+
100
+ def log_debug(message)
101
+ Multiwoven::Integrations::Service.logger.debug(message)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end