multiwoven-integrations 0.1.34 → 0.1.35

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