multiwoven-integrations 0.1.34 → 0.1.35

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32abb3bef8987a8a76828570507e207089efb5c7c59dd21093774beeef21a265
4
- data.tar.gz: 9920eb296b386ef3add818a1e7b59ef26e7e8c32aee8a21e8295b82deebc4d44
3
+ metadata.gz: 154bbe9f4e36d95f4cd7fdb8094077e2ff724a3eef95ec76264950747d619fe2
4
+ data.tar.gz: b3da4210a997a0143b637a32d4d4b0dc6ba741742e5a1f73a3a465c8187479de
5
5
  SHA512:
6
- metadata.gz: 33846b75d3a91aedf93fa367d0cd2385f7dd06066015a782cd149dd1cedefd055f29c9c007062988b13a18b9eb4efac1afd250a4b47531c1fcfd5d0b0f5619c9
7
- data.tar.gz: 6faa53d0929c0d5ae12da7467e56573e86e1ff3b1898f96762150440a19fff351796198e2fe03f72a12a7008762391ac4714a3f5d5a9703602e6acaf328d27ba
6
+ metadata.gz: '052295ce22be1a08561f2d907bb1974c465302f2ec12cd6ae50afa976354bc13eb9ac0d178cd1b9eca86945cb18c4cb316ef05a44e6deac14ccc49f03fb57029'
7
+ data.tar.gz: 35e541a5b3ea70f9761ff0c54b527a6334fcc91d5321018d4ce926163f8611eb4a4db714a2b36ad01c8e869854544ec63142bef2e2e99bd7cc0d3d2e022ebe2d
@@ -18,6 +18,8 @@ module Multiwoven
18
18
  SNOWFLAKE_DRIVER_PATH = ENV["SNOWFLAKE_DRIVER_PATH"] || SNOWFLAKE_MAC_DRIVER_PATH
19
19
  DATABRICKS_DRIVER_PATH = ENV["DATABRICKS_DRIVER_PATH"] || DATABRICKS_MAC_DRIVER_PATH
20
20
 
21
+ JSON_SCHEMA_URL = "https://json-schema.org/draft-07/schema#"
22
+
21
23
  # CONNECTORS
22
24
  KLAVIYO_AUTH_ENDPOINT = "https://a.klaviyo.com/api/lists/"
23
25
  KLAVIYO_AUTH_PAYLOAD = {
@@ -31,14 +33,16 @@ module Multiwoven
31
33
 
32
34
  FACEBOOK_AUDIENCE_GET_ALL_ACCOUNTS = "https://graph.facebook.com/v18.0/me/adaccounts?fields=id,name"
33
35
 
36
+ AIRTABLE_URL_BASE = "https://api.airtable.com/v0/"
37
+ AIRTABLE_BASES_ENDPOINT = "https://api.airtable.com/v0/meta/bases"
38
+ AIRTABLE_GET_BASE_SCHEMA_ENDPOINT = "https://api.airtable.com/v0/meta/bases/{baseId}/tables"
39
+
34
40
  # HTTP
35
41
  HTTP_GET = "GET"
36
42
  HTTP_POST = "POST"
37
43
  HTTP_PUT = "PUT"
38
44
  HTTP_DELETE = "DELETE"
39
45
 
40
- JSON_SCHEMA_URL = "http://json-schema.org/draft-07/schema#"
41
-
42
46
  # google sheets
43
47
  GOOGLE_SHEETS_SCOPE = "https://www.googleapis.com/auth/drive"
44
48
  GOOGLE_SPREADSHEET_ID_REGEX = %r{/d/([-\w]{20,})/}.freeze
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schema_helper"
4
+ module Multiwoven
5
+ module Integrations
6
+ module Destination
7
+ module Airtable
8
+ include Multiwoven::Integrations::Core
9
+ class Client < DestinationConnector # rubocop:disable Metrics/ClassLength
10
+ MAX_CHUNK_SIZE = 10
11
+ def check_connection(connection_config)
12
+ connection_config = connection_config.with_indifferent_access
13
+ bases = Multiwoven::Integrations::Core::HttpClient.request(
14
+ AIRTABLE_BASES_ENDPOINT,
15
+ HTTP_GET,
16
+ headers: auth_headers(connection_config[:api_key])
17
+ )
18
+ if success?(bases)
19
+ base_id_exists?(bases, connection_config[:base_id])
20
+ success_status
21
+ else
22
+ failure_status(nil)
23
+ end
24
+ rescue StandardError => e
25
+ failure_status(e)
26
+ end
27
+
28
+ def discover(connection_config)
29
+ connection_config = connection_config.with_indifferent_access
30
+ base_id = connection_config[:base_id]
31
+ api_key = connection_config[:api_key]
32
+
33
+ bases = Multiwoven::Integrations::Core::HttpClient.request(
34
+ AIRTABLE_BASES_ENDPOINT,
35
+ HTTP_GET,
36
+ headers: auth_headers(api_key)
37
+ )
38
+
39
+ base = extract_bases(bases).find { |b| b["id"] == base_id }
40
+ base_name = base["name"]
41
+
42
+ schema = Multiwoven::Integrations::Core::HttpClient.request(
43
+ AIRTABLE_GET_BASE_SCHEMA_ENDPOINT.gsub("{baseId}", base_id),
44
+ HTTP_GET,
45
+ headers: auth_headers(api_key)
46
+ )
47
+
48
+ catalog = build_catalog_from_schema(extract_body(schema), base_id, base_name)
49
+ catalog.to_multiwoven_message
50
+ rescue StandardError => e
51
+ handle_exception("AIRTABLE:DISCOVER:EXCEPTION", "error", e)
52
+ end
53
+
54
+ def write(sync_config, records, _action = "create")
55
+ connection_config = sync_config.destination.connection_specification.with_indifferent_access
56
+ api_key = connection_config[:api_key]
57
+ url = sync_config.stream.url
58
+ write_success = 0
59
+ write_failure = 0
60
+ records.each_slice(MAX_CHUNK_SIZE) do |chunk|
61
+ payload = create_payload(chunk)
62
+ response = Multiwoven::Integrations::Core::HttpClient.request(
63
+ url,
64
+ sync_config.stream.request_method,
65
+ payload: payload,
66
+ headers: auth_headers(api_key)
67
+ )
68
+ if success?(response)
69
+ write_success += chunk.size
70
+ else
71
+ write_failure += chunk.size
72
+ end
73
+ rescue StandardError => e
74
+ handle_exception("AIRTABLE:RECORD:WRITE:EXCEPTION", "error", e)
75
+ write_failure += chunk.size
76
+ end
77
+
78
+ tracker = Multiwoven::Integrations::Protocol::TrackingMessage.new(
79
+ success: write_success,
80
+ failed: write_failure
81
+ )
82
+ tracker.to_multiwoven_message
83
+ rescue StandardError => e
84
+ handle_exception("AIRTABLE:WRITE:EXCEPTION", "error", e)
85
+ end
86
+
87
+ private
88
+
89
+ def create_payload(records)
90
+ {
91
+ "records" => records.map do |record|
92
+ {
93
+ "fields" => record
94
+ }
95
+ end
96
+ }
97
+ end
98
+
99
+ def auth_headers(access_token)
100
+ {
101
+ "Accept" => "application/json",
102
+ "Authorization" => "Bearer #{access_token}",
103
+ "Content-Type" => "application/json"
104
+ }
105
+ end
106
+
107
+ def base_id_exists?(bases, base_id)
108
+ return if extract_data(bases).any? { |base| base["id"] == base_id }
109
+
110
+ raise ArgumentError, "base_id not found"
111
+ end
112
+
113
+ def extract_bases(response)
114
+ response_body = extract_body(response)
115
+ response_body["bases"] if response_body
116
+ end
117
+
118
+ def extract_body(response)
119
+ response_body = response.body
120
+ JSON.parse(response_body) if response_body
121
+ end
122
+
123
+ def load_catalog
124
+ read_json(CATALOG_SPEC_PATH)
125
+ end
126
+
127
+ def create_stream(table, base_id, base_name)
128
+ {
129
+ name: "#{base_name}/#{SchemaHelper.clean_name(table["name"])}",
130
+ action: "create",
131
+ method: HTTP_POST,
132
+ url: "#{AIRTABLE_URL_BASE}#{base_id}/#{table["id"]}",
133
+ json_schema: SchemaHelper.get_json_schema(table),
134
+ supported_sync_modes: %w[incremental],
135
+ batch_support: true,
136
+ batch_size: 10
137
+
138
+ }.with_indifferent_access
139
+ end
140
+
141
+ def build_catalog_from_schema(schema, base_id, base_name)
142
+ catalog = build_catalog(load_catalog)
143
+ schema["tables"].each do |table|
144
+ catalog.streams << build_stream(create_stream(table, base_id, base_name))
145
+ end
146
+ catalog
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ 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": "Airtable",
4
+ "title": "airtable",
5
+ "connector_type": "destination",
6
+ "category": "Productivity Tools",
7
+ "documentation_url": "https://docs.multiwoven.com/destinations/productivity-tools/airtable",
8
+ "github_issue_label": "destination-airtable",
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,22 @@
1
+ {
2
+ "documentation_url": "https://docs.multiwoven.com/integrations/destination/airtable",
3
+ "stream_type": "dynamic",
4
+ "connection_specification": {
5
+ "$schema": "http://json-schema.org/draft-07/schema#",
6
+ "title": "Airtable",
7
+ "type": "object",
8
+ "required": ["api_key", "base_id"],
9
+ "properties": {
10
+ "api_key": {
11
+ "type": "string",
12
+ "title": "Personal access token",
13
+ "order": 0
14
+ },
15
+ "base_id": {
16
+ "type": "string",
17
+ "title": "Airtable base id",
18
+ "order": 1
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="27" shape-rendering="geometricPrecision" viewBox="0 0 200 170">
2
+ <path fill="#FCB400" d="M90.039 12.367L24.079 39.66c-3.667 1.519-3.63 6.729.062 8.192l66.235 26.266a24.575 24.575 0 0018.12 0l66.236-26.266c3.69-1.463 3.729-6.673.06-8.191l-65.958-27.294a24.578 24.578 0 00-18.795 0"></path>
3
+ <path fill="#18BFFF" d="M105.312 88.46v65.617c0 3.12 3.147 5.258 6.048 4.108l73.806-28.648a4.418 4.418 0 002.79-4.108V59.813c0-3.121-3.147-5.258-6.048-4.108l-73.806 28.648a4.42 4.42 0 00-2.79 4.108"></path>
4
+ <path fill="#F82B60" d="M88.078 91.846l-21.904 10.576-2.224 1.075-46.238 22.155c-2.93 1.414-6.672-.722-6.672-3.978V60.088c0-1.178.604-2.195 1.414-2.96a5.024 5.024 0 011.12-.84c1.104-.663 2.68-.84 4.02-.31L87.71 83.76c3.564 1.414 3.844 6.408.368 8.087"></path>
5
+ <path fill="rgba(0, 0, 0, 0.25)" d="M88.078 91.846l-21.904 10.576-53.72-45.295a5.024 5.024 0 011.12-.839c1.104-.663 2.68-.84 4.02-.31L87.71 83.76c3.564 1.414 3.844 6.408.368 8.087"></path>
6
+ </svg>
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multiwoven
4
+ module Integrations
5
+ module Destination
6
+ module Airtable
7
+ module SchemaHelper
8
+ include Core::Constants
9
+
10
+ module_function
11
+
12
+ def clean_name(name_str)
13
+ name_str.strip.gsub(" ", "_")
14
+ end
15
+
16
+ def get_json_schema(table)
17
+ fields = table["fields"] || {}
18
+ properties = fields.each_with_object({}) do |field, props|
19
+ name, schema = process_field(field)
20
+ props[name] = schema
21
+ end
22
+
23
+ build_schema(properties)
24
+ end
25
+
26
+ def process_field(field)
27
+ name = clean_name(field.fetch("name", ""))
28
+ original_type = field.fetch("type", "")
29
+ options = field.fetch("options", {})
30
+
31
+ schema = determine_schema(original_type, options)
32
+ [name, schema]
33
+ end
34
+
35
+ def determine_schema(original_type, options)
36
+ if COMPLEX_AIRTABLE_TYPES.keys.include?(original_type)
37
+ complex_type = deep_copy(COMPLEX_AIRTABLE_TYPES[original_type])
38
+ adjust_complex_type(original_type, complex_type, options)
39
+ elsif SIMPLE_AIRTABLE_TYPES.keys.include?(original_type)
40
+ simple_type_schema(original_type, options)
41
+ else
42
+ SCHEMA_TYPES[:STRING]
43
+ end
44
+ end
45
+
46
+ def adjust_complex_type(original_type, complex_type, options)
47
+ exec_type = options.dig("result", "type") || "simpleText"
48
+ if complex_type == SCHEMA_TYPES[:ARRAY_WITH_ANY]
49
+ adjust_array_with_any(original_type, complex_type, exec_type, options)
50
+ else
51
+ complex_type
52
+ end
53
+ end
54
+
55
+ def adjust_array_with_any(original_type, complex_type, exec_type, options)
56
+ if original_type == "formula" && %w[number currency percent duration].include?(exec_type)
57
+ complex_type = SCHEMA_TYPES[:NUMBER]
58
+ elsif original_type == "formula" && ARRAY_FORMULAS.none? { |x| options.fetch("formula", "").start_with?(x) }
59
+ complex_type = SCHEMA_TYPES[:STRING]
60
+ elsif SIMPLE_AIRTABLE_TYPES.keys.include?(exec_type)
61
+ complex_type["items"] = deep_copy(SIMPLE_AIRTABLE_TYPES[exec_type])
62
+ else
63
+ complex_type["items"] = SCHEMA_TYPES[:STRING]
64
+ end
65
+ complex_type
66
+ end
67
+
68
+ def simple_type_schema(original_type, options)
69
+ exec_type = options.dig("result", "type") || original_type
70
+ deep_copy(SIMPLE_AIRTABLE_TYPES[exec_type])
71
+ end
72
+
73
+ def build_schema(properties)
74
+ {
75
+ "$schema" => JSON_SCHEMA_URL,
76
+ "type" => "object",
77
+ "additionalProperties" => true,
78
+ "properties" => properties
79
+ }
80
+ end
81
+
82
+ def deep_copy(object)
83
+ Marshal.load(Marshal.dump(object))
84
+ end
85
+
86
+ SCHEMA_TYPES = {
87
+ STRING: { "type": %w[null string] },
88
+ NUMBER: { "type": %w[null number] },
89
+ BOOLEAN: { "type": %w[null boolean] },
90
+ DATE: { "type": %w[null string], "format": "date" },
91
+ DATETIME: { "type": %w[null string], "format": "date-time" },
92
+ ARRAY_WITH_STRINGS: { "type": %w[null array], "items": { "type": %w[null string] } },
93
+ ARRAY_WITH_ANY: { "type": %w[null array], "items": {} }
94
+ }.freeze.with_indifferent_access
95
+
96
+ SIMPLE_AIRTABLE_TYPES = {
97
+ "multipleAttachments" => SCHEMA_TYPES[:STRING],
98
+ "autoNumber" => SCHEMA_TYPES[:NUMBER],
99
+ "barcode" => SCHEMA_TYPES[:STRING],
100
+ "button" => SCHEMA_TYPES[:STRING],
101
+ "checkbox" => :BOOLEAN,
102
+ "singleCollaborator" => SCHEMA_TYPES[:STRING],
103
+ "count" => SCHEMA_TYPES[:NUMBER],
104
+ "createdBy" => SCHEMA_TYPES[:STRING],
105
+ "createdTime" => SCHEMA_TYPES[:DATETIME],
106
+ "currency" => SCHEMA_TYPES[:NUMBER],
107
+ "email" => SCHEMA_TYPES[:STRING],
108
+ "date" => SCHEMA_TYPES[:DATE],
109
+ "dateTime" => SCHEMA_TYPES[:DATETIME],
110
+ "duration" => SCHEMA_TYPES[:NUMBER],
111
+ "lastModifiedBy" => SCHEMA_TYPES[:STRING],
112
+ "lastModifiedTime" => SCHEMA_TYPES[:DATETIME],
113
+ "multipleRecordLinks" => SCHEMA_TYPES[:ARRAY_WITH_STRINGS],
114
+ "multilineText" => SCHEMA_TYPES[:STRING],
115
+ "multipleCollaborators" => SCHEMA_TYPES[:ARRAY_WITH_STRINGS],
116
+ "multipleSelects" => SCHEMA_TYPES[:ARRAY_WITH_STRINGS],
117
+ "number" => SCHEMA_TYPES[:NUMBER],
118
+ "percent" => SCHEMA_TYPES[:NUMBER],
119
+ "phoneNumber" => SCHEMA_TYPES[:STRING],
120
+ "rating" => SCHEMA_TYPES[:NUMBER],
121
+ "richText" => SCHEMA_TYPES[:STRING],
122
+ "singleLineText" => SCHEMA_TYPES[:STRING],
123
+ "singleSelect" => SCHEMA_TYPES[:STRING],
124
+ "externalSyncSource" => SCHEMA_TYPES[:STRING],
125
+ "url" => SCHEMA_TYPES[:STRING],
126
+ "simpleText" => SCHEMA_TYPES[:STRING]
127
+ }.freeze
128
+
129
+ COMPLEX_AIRTABLE_TYPES = {
130
+ "formula" => SCHEMA_TYPES[:ARRAY_WITH_ANY],
131
+ "lookup" => SCHEMA_TYPES[:ARRAY_WITH_ANY],
132
+ "multipleLookupValues" => SCHEMA_TYPES[:ARRAY_WITH_ANY],
133
+ "rollup" => SCHEMA_TYPES[:ARRAY_WITH_ANY]
134
+ }.freeze.with_indifferent_access
135
+
136
+ ARRAY_FORMULAS = %w[ARRAYCOMPACT ARRAYFLATTEN ARRAYUNIQUE ARRAYSLICE].freeze
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Multiwoven
4
4
  module Integrations
5
- VERSION = "0.1.34"
5
+ VERSION = "0.1.35"
6
6
 
7
7
  ENABLED_SOURCES = %w[
8
8
  Snowflake
@@ -19,6 +19,7 @@ module Multiwoven
19
19
  Slack
20
20
  Hubspot
21
21
  GoogleSheets
22
+ Airtable
22
23
  ].freeze
23
24
  end
24
25
  end
@@ -48,6 +48,7 @@ require_relative "integrations/destination/facebook_custom_audience/client"
48
48
  require_relative "integrations/destination/slack/client"
49
49
  require_relative "integrations/destination/hubspot/client"
50
50
  require_relative "integrations/destination/google_sheets/client"
51
+ require_relative "integrations/destination/airtable/client"
51
52
 
52
53
  module Multiwoven
53
54
  module Integrations
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multiwoven-integrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.34
4
+ version: 0.1.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Subin T P
@@ -330,6 +330,12 @@ files:
330
330
  - lib/multiwoven/integrations/core/rate_limiter.rb
331
331
  - lib/multiwoven/integrations/core/source_connector.rb
332
332
  - lib/multiwoven/integrations/core/utils.rb
333
+ - lib/multiwoven/integrations/destination/airtable/client.rb
334
+ - lib/multiwoven/integrations/destination/airtable/config/catalog.json
335
+ - lib/multiwoven/integrations/destination/airtable/config/meta.json
336
+ - lib/multiwoven/integrations/destination/airtable/config/spec.json
337
+ - lib/multiwoven/integrations/destination/airtable/icon.svg
338
+ - lib/multiwoven/integrations/destination/airtable/schema_helper.rb
333
339
  - lib/multiwoven/integrations/destination/facebook_custom_audience/client.rb
334
340
  - lib/multiwoven/integrations/destination/facebook_custom_audience/config/catalog.json
335
341
  - lib/multiwoven/integrations/destination/facebook_custom_audience/config/meta.json