webhookdb 1.2.2 → 1.3.1
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 +4 -4
- data/admin-dist/assets/index-6aebf805.js +264 -0
- data/admin-dist/favicon.ico +0 -0
- data/admin-dist/index.html +130 -0
- data/admin-dist/manifest.json +15 -0
- data/data/messages/replicators/url-recorder.liquid +20 -0
- data/data/messages/templates/errors/signalwire_send_sms.email.liquid +31 -0
- data/data/messages/web/install-customer-login.liquid +6 -5
- data/data/messages/web/install-error.liquid +1 -1
- data/data/messages/web/install-forbidden.liquid +25 -0
- data/data/messages/web/install-org-chooser.liquid +40 -0
- data/data/messages/web/install-success.liquid +2 -1
- data/data/messages/web/install.liquid +2 -1
- data/data/messages/web/partials/head.liquid +2 -0
- data/data/messages/web/styles.liquid +24 -0
- data/db/migrations/041_views.rb +20 -0
- data/db/migrations/042_sint_lock.rb +10 -0
- data/db/migrations/043_text_search.rb +28 -0
- data/db/migrations/044_oauth_session_token_cache.rb +21 -0
- data/integration/auth_spec.rb +2 -2
- data/lib/sequel/plugins/text_searchable.rb +165 -0
- data/lib/sequel/text_searchable.rb +42 -0
- data/lib/webhookdb/admin_api/auth.rb +24 -3
- data/lib/webhookdb/admin_api/data_provider.rb +196 -0
- data/lib/webhookdb/admin_api/entities.rb +143 -28
- data/lib/webhookdb/admin_api.rb +0 -2
- data/lib/webhookdb/api/auth.rb +5 -6
- data/lib/webhookdb/api/db.rb +31 -6
- data/lib/webhookdb/api/entities.rb +7 -1
- data/lib/webhookdb/api/helpers.rb +6 -25
- data/lib/webhookdb/api/install.rb +204 -79
- data/lib/webhookdb/api/organizations.rb +14 -12
- data/lib/webhookdb/api/saved_queries.rb +9 -3
- data/lib/webhookdb/api/saved_views.rb +99 -0
- data/lib/webhookdb/api/service_integrations.rb +15 -9
- data/lib/webhookdb/api/subscriptions.rb +3 -1
- data/lib/webhookdb/api/sync_targets.rb +9 -7
- data/lib/webhookdb/api/system.rb +1 -0
- data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
- data/lib/webhookdb/apps.rb +30 -7
- data/lib/webhookdb/async/audit_logger.rb +2 -0
- data/lib/webhookdb/async.rb +5 -0
- data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
- data/lib/webhookdb/backfill_job.rb +9 -0
- data/lib/webhookdb/customer.rb +5 -0
- data/lib/webhookdb/database_document.rb +1 -1
- data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
- data/lib/webhookdb/db_adapter.rb +20 -4
- data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
- data/lib/webhookdb/fixtures/organizations.rb +5 -0
- data/lib/webhookdb/fixtures/roles.rb +14 -0
- data/lib/webhookdb/fixtures/saved_views.rb +25 -0
- data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
- data/lib/webhookdb/http.rb +8 -2
- data/lib/webhookdb/icalendar.rb +3 -0
- data/lib/webhookdb/idempotency.rb +69 -22
- data/lib/webhookdb/increase.rb +69 -21
- data/lib/webhookdb/intercom.rb +10 -3
- data/lib/webhookdb/jobs/backfill.rb +3 -1
- data/lib/webhookdb/jobs/emailer.rb +0 -1
- data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
- data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
- data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
- data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
- data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
- data/lib/webhookdb/message/body.rb +6 -4
- data/lib/webhookdb/message/delivery.rb +2 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
- data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
- data/lib/webhookdb/oauth/fake_provider.rb +44 -0
- data/lib/webhookdb/oauth/front_provider.rb +1 -2
- data/lib/webhookdb/oauth/increase_provider.rb +80 -0
- data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
- data/lib/webhookdb/oauth/session.rb +20 -0
- data/lib/webhookdb/oauth.rb +7 -21
- data/lib/webhookdb/organization/alerting.rb +2 -0
- data/lib/webhookdb/organization/database_migration.rb +3 -0
- data/lib/webhookdb/organization.rb +37 -6
- data/lib/webhookdb/organization_membership.rb +14 -7
- data/lib/webhookdb/postgres.rb +2 -0
- data/lib/webhookdb/replicator/base.rb +1 -0
- data/lib/webhookdb/replicator/docgen.rb +9 -1
- data/lib/webhookdb/replicator/fake.rb +2 -3
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
- data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
- data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
- data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
- data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
- data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
- data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
- data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
- data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
- data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
- data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
- data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
- data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
- data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
- data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
- data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
- data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
- data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
- data/lib/webhookdb/replicator/webhook_request.rb +4 -0
- data/lib/webhookdb/replicator.rb +8 -0
- data/lib/webhookdb/role.rb +5 -2
- data/lib/webhookdb/saved_query.rb +23 -0
- data/lib/webhookdb/saved_view.rb +73 -0
- data/lib/webhookdb/sentry.rb +2 -0
- data/lib/webhookdb/service/entities.rb +0 -4
- data/lib/webhookdb/service/helpers.rb +5 -0
- data/lib/webhookdb/service/middleware.rb +9 -0
- data/lib/webhookdb/service/types.rb +10 -8
- data/lib/webhookdb/service/validators.rb +1 -2
- data/lib/webhookdb/service/view_api.rb +1 -1
- data/lib/webhookdb/service_integration.rb +17 -15
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
- data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
- data/lib/webhookdb/subscription.rb +2 -0
- data/lib/webhookdb/sync_target.rb +10 -2
- data/lib/webhookdb/tasks/message.rb +3 -1
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
- data/lib/webhookdb/webhook_subscription.rb +2 -0
- metadata +57 -9
- data/lib/webhookdb/admin_api/customers.rb +0 -63
- data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
- data/lib/webhookdb/admin_api/roles.rb +0 -15
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Webhookdb::Replicator::UrlRecorderV1 < Webhookdb::Replicator::Base
|
|
4
|
+
include Appydays::Loggable
|
|
5
|
+
|
|
6
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
7
|
+
def self.descriptor
|
|
8
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
9
|
+
name: "url_recorder_v1",
|
|
10
|
+
ctor: ->(sint) { self.new(sint) },
|
|
11
|
+
feature_roles: [],
|
|
12
|
+
resource_name_singular: "URL Recorder",
|
|
13
|
+
supports_webhooks: true,
|
|
14
|
+
supports_backfill: false,
|
|
15
|
+
description: "Record any visit to the webhook URL for later inspection. " \
|
|
16
|
+
"Useful for recording scans, like visiting QR Code. After the webhook, " \
|
|
17
|
+
"visitors can be redirected, or shown a Markdown page.",
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def _remote_key_column = Webhookdb::Replicator::Column.new(:unique_id, BIGINT)
|
|
22
|
+
|
|
23
|
+
def requires_sequence? = true
|
|
24
|
+
|
|
25
|
+
def _denormalized_columns
|
|
26
|
+
col = Webhookdb::Replicator::Column
|
|
27
|
+
return [
|
|
28
|
+
col.new(:inserted_at, TIMESTAMP, index: true),
|
|
29
|
+
col.new(:request_method, TEXT),
|
|
30
|
+
col.new(:path, TEXT),
|
|
31
|
+
col.new(:full_url, TEXT),
|
|
32
|
+
col.new(:user_agent, TEXT),
|
|
33
|
+
col.new(:ip, TEXT),
|
|
34
|
+
col.new(:content_type, TEXT),
|
|
35
|
+
col.new(:parsed_query, OBJECT),
|
|
36
|
+
col.new(:parsed_body, OBJECT),
|
|
37
|
+
col.new(:raw_body, TEXT),
|
|
38
|
+
]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def _timestamp_column_name = :inserted_at
|
|
42
|
+
def _resource_and_event(_request) = [{}, nil]
|
|
43
|
+
|
|
44
|
+
def _update_where_expr = self.qualified_table_sequel_identifier[:inserted_at] < Sequel[:excluded][:inserted_at]
|
|
45
|
+
|
|
46
|
+
def _prepare_for_insert(_resource, event, request, enrichment)
|
|
47
|
+
# rr = Rack::Request.new
|
|
48
|
+
rr = request.rack_request
|
|
49
|
+
raise Webhookdb::InvalidPrecondition, "#{request} must have rack_request set" if rr.nil?
|
|
50
|
+
r = {
|
|
51
|
+
"unique_id" => self.service_integration.sequence_nextval,
|
|
52
|
+
"inserted_at" => Time.now,
|
|
53
|
+
"request_method" => rr.request_method,
|
|
54
|
+
"path" => rr.path,
|
|
55
|
+
"parsed_query" => rr.GET,
|
|
56
|
+
"raw_query" => rr.query_string,
|
|
57
|
+
"full_url" => rr.url,
|
|
58
|
+
"user_agent" => rr.user_agent,
|
|
59
|
+
"ip" => rr.ip,
|
|
60
|
+
"content_type" => rr.content_type,
|
|
61
|
+
"raw_body" => nil,
|
|
62
|
+
"parsed_body" => nil,
|
|
63
|
+
}
|
|
64
|
+
if !request.body.is_a?(String)
|
|
65
|
+
# If we were able to parse the request body (usually means it's JSON), store it.
|
|
66
|
+
r["parsed_body"] = request.body
|
|
67
|
+
elsif rr.POST.present?
|
|
68
|
+
# If Rack was able to parse the request body (usually means it's form encoded), store it.
|
|
69
|
+
r["parsed_body"] = rr.POST
|
|
70
|
+
else
|
|
71
|
+
# Store the raw body if nothing can parse it.
|
|
72
|
+
r["raw_body"] = request.body
|
|
73
|
+
end
|
|
74
|
+
return super(r, event, request, enrichment)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def _resource_to_data(*) = {}
|
|
78
|
+
|
|
79
|
+
def _webhook_response(_request) = self.redirect? ? self._redirect_response : self._page_response
|
|
80
|
+
|
|
81
|
+
def process_webhooks_synchronously? = true
|
|
82
|
+
|
|
83
|
+
def synchronous_processing_response_body(*)
|
|
84
|
+
resp = self.redirect? ? self._redirect_response : self._page_response
|
|
85
|
+
return resp.body
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def redirect? = self.service_integration.api_url =~ %r{^https?://}
|
|
89
|
+
|
|
90
|
+
def _redirect_response
|
|
91
|
+
headers = {"Location" => self.service_integration.api_url, "Content-Type" => "text/plain"}
|
|
92
|
+
return Webhookdb::WebhookResponse.new(status: 302, headers:, body: "")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def _page_response
|
|
96
|
+
headers = {"Content-Type" => "text/html; charset=UTF-8"}
|
|
97
|
+
content = self.service_integration.api_url
|
|
98
|
+
content_is_doc = content.start_with?("<!DOCTYPE") || content.starts_with?("<html")
|
|
99
|
+
body = if content_is_doc
|
|
100
|
+
content
|
|
101
|
+
else
|
|
102
|
+
tmpl_file = File.open(Webhookdb::DATA_DIR + "messages/replicators/url-recorder.liquid")
|
|
103
|
+
liquid_tmpl = Liquid::Template.parse(tmpl_file.read)
|
|
104
|
+
liquid_tmpl.render!({"content" => content})
|
|
105
|
+
end
|
|
106
|
+
return Webhookdb::WebhookResponse.new(status: 200, headers:, body:)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def calculate_webhook_state_machine
|
|
110
|
+
step = Webhookdb::Replicator::StateMachineStep.new
|
|
111
|
+
if self.service_integration.api_url.blank?
|
|
112
|
+
step.output = %(After users visit the WebhookDB endpoint,
|
|
113
|
+
they can either be redirected to a location of your own,
|
|
114
|
+
or we'll render an HTML page to show them.
|
|
115
|
+
|
|
116
|
+
To use a redirect, input a URL starting with 'https://'.
|
|
117
|
+
|
|
118
|
+
To render a page, paste in the HTML:
|
|
119
|
+
|
|
120
|
+
- If the text starts with an `html` tag, it will be used as-is
|
|
121
|
+
for the page's HTML, so you can use your own styles.
|
|
122
|
+
- Otherwise we assume the content is a relatively simple message,
|
|
123
|
+
and it's rendered with basic WebhookDB styles.)
|
|
124
|
+
return step.prompting("URL, HTML, or text").api_url(self.service_integration)
|
|
125
|
+
end
|
|
126
|
+
step.output = %(
|
|
127
|
+
All set! Every visit to
|
|
128
|
+
#{self.webhook_endpoint}
|
|
129
|
+
will be recorded.
|
|
130
|
+
|
|
131
|
+
If you want to modify what users see after the visit is recorded,
|
|
132
|
+
run `webhookdb integration reset #{self.descriptor.name}.
|
|
133
|
+
|
|
134
|
+
#{self._query_help_output})
|
|
135
|
+
return step.completed
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -2,4 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
class Webhookdb::Replicator::WebhookRequest < Webhookdb::TypedStruct
|
|
4
4
|
attr_accessor :body, :headers, :path, :method
|
|
5
|
+
# @!attribute rack_request
|
|
6
|
+
# When a webhook is processed synchronously, this will be set to the Rack::Request.
|
|
7
|
+
# Normal (async) webhook processing does not have this available.
|
|
8
|
+
attr_accessor :rack_request
|
|
5
9
|
end
|
data/lib/webhookdb/replicator.rb
CHANGED
|
@@ -70,6 +70,12 @@ class Webhookdb::Replicator
|
|
|
70
70
|
# for example in case 'backfill' is called but not supported.
|
|
71
71
|
attr_reader :documentation_url
|
|
72
72
|
|
|
73
|
+
# If this integration uses /v1/install to set up,
|
|
74
|
+
# or some other link like a marketplace URL,
|
|
75
|
+
# provide it here. Note that you can add a redirect to Webhookdb::Apps::REDIRECTS
|
|
76
|
+
# to provide a pretty, arbitrary URL.
|
|
77
|
+
attr_reader :install_url
|
|
78
|
+
|
|
73
79
|
# Markdown description of this replicator.
|
|
74
80
|
attr_reader :description
|
|
75
81
|
|
|
@@ -94,6 +100,7 @@ class Webhookdb::Replicator
|
|
|
94
100
|
description: nil,
|
|
95
101
|
enterprise: false,
|
|
96
102
|
documentation_url: nil,
|
|
103
|
+
install_url: nil,
|
|
97
104
|
documentable: nil
|
|
98
105
|
)
|
|
99
106
|
raise ArgumentError, "must support one or both of webhooks and backfill" unless
|
|
@@ -107,6 +114,7 @@ class Webhookdb::Replicator
|
|
|
107
114
|
dependency_descriptor:,
|
|
108
115
|
documentation_url:,
|
|
109
116
|
api_docs_url:,
|
|
117
|
+
install_url:,
|
|
110
118
|
enterprise:
|
|
111
119
|
)
|
|
112
120
|
@ctor = ctor.is_a?(Class) ? ctor.method(:new) : ctor
|
data/lib/webhookdb/role.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "webhookdb/postgres/model"
|
|
4
4
|
|
|
5
5
|
class Webhookdb::Role < Webhookdb::Postgres::Model(:roles)
|
|
6
|
+
plugin :text_searchable, terms: [:name]
|
|
7
|
+
|
|
6
8
|
# n.b. Because of the uniqueness constraint on "name", there is only one "admin" role. Its meaning
|
|
7
9
|
# depends on the context: if the customer has this role, they are an admin; if the org membership has
|
|
8
10
|
# this role, the customer is an org admin.
|
|
@@ -30,8 +32,9 @@ end
|
|
|
30
32
|
# Table: roles
|
|
31
33
|
# ---------------------------------------------------------------------------------------------------------------------------
|
|
32
34
|
# Columns:
|
|
33
|
-
# id
|
|
34
|
-
# name
|
|
35
|
+
# id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
|
|
36
|
+
# name | text | NOT NULL
|
|
37
|
+
# text_search | tsvector |
|
|
35
38
|
# Indexes:
|
|
36
39
|
# roles_pkey | PRIMARY KEY btree (id)
|
|
37
40
|
# roles_name_key | UNIQUE btree (name)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Webhookdb::SavedQuery < Webhookdb::Postgres::Model(:saved_queries)
|
|
4
4
|
plugin :timestamps
|
|
5
|
+
plugin :text_searchable, terms: [:organization, :created_by]
|
|
5
6
|
|
|
6
7
|
CLI_EDITABLE_FIELDS = ["description", "sql", "public"].freeze
|
|
7
8
|
INFO_FIELDS = {
|
|
@@ -26,3 +27,25 @@ class Webhookdb::SavedQuery < Webhookdb::Postgres::Model(:saved_queries)
|
|
|
26
27
|
|
|
27
28
|
def run_url = "#{Webhookdb.api_url}/v1/saved_queries/#{self.opaque_id}/run"
|
|
28
29
|
end
|
|
30
|
+
|
|
31
|
+
# Table: saved_queries
|
|
32
|
+
# ------------------------------------------------------------------------------------------------------
|
|
33
|
+
# Columns:
|
|
34
|
+
# id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
|
|
35
|
+
# created_at | timestamp with time zone | NOT NULL DEFAULT now()
|
|
36
|
+
# updated_at | timestamp with time zone |
|
|
37
|
+
# organization_id | integer | NOT NULL
|
|
38
|
+
# created_by_id | integer |
|
|
39
|
+
# opaque_id | text | NOT NULL
|
|
40
|
+
# description | text | NOT NULL
|
|
41
|
+
# sql | text | NOT NULL
|
|
42
|
+
# public | boolean | NOT NULL DEFAULT false
|
|
43
|
+
# text_search | tsvector |
|
|
44
|
+
# Indexes:
|
|
45
|
+
# saved_queries_pkey | PRIMARY KEY btree (id)
|
|
46
|
+
# saved_queries_opaque_id_key | UNIQUE btree (opaque_id)
|
|
47
|
+
# saved_queries_organization_id_index | btree (organization_id)
|
|
48
|
+
# Foreign key constraints:
|
|
49
|
+
# saved_queries_created_by_id_fkey | (created_by_id) REFERENCES customers(id) ON DELETE SET NULL
|
|
50
|
+
# saved_queries_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
51
|
+
# ------------------------------------------------------------------------------------------------------
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Webhookdb::SavedView < Webhookdb::Postgres::Model(:saved_views)
|
|
4
|
+
plugin :timestamps
|
|
5
|
+
plugin :text_searchable, terms: [:organization, :created_by]
|
|
6
|
+
|
|
7
|
+
DOCS_URL = "https://docs.webhookdb.com/docs/integrating/saved-views.html"
|
|
8
|
+
|
|
9
|
+
class InvalidQuery < Webhookdb::InvalidInput; end
|
|
10
|
+
|
|
11
|
+
many_to_one :organization, class: "Webhookdb::Organization"
|
|
12
|
+
many_to_one :created_by, class: "Webhookdb::Customer"
|
|
13
|
+
|
|
14
|
+
def self.feature_role
|
|
15
|
+
return Webhookdb.cached_get("saved-view-feature-role") do
|
|
16
|
+
Webhookdb::Role.find_or_create_or_find(name: "saved_views")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.create_or_replace(organization:, sql:, name:, **kw)
|
|
21
|
+
Webhookdb::DBAdapter.validate_identifier!(name, type: "view")
|
|
22
|
+
self.db.transaction do
|
|
23
|
+
sv = self.find_or_create_or_find(organization:, name:) do |new|
|
|
24
|
+
new.sql = sql
|
|
25
|
+
end
|
|
26
|
+
sv.update(sql:, **kw)
|
|
27
|
+
|
|
28
|
+
# Verify that the underlying query is readonly, by running it as a readonly user.
|
|
29
|
+
if (_, errmsg = organization.execute_readonly_query_with_help(sql)) && errmsg.present?
|
|
30
|
+
raise InvalidQuery, errmsg
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Create the view now that we've asserted it's readonly
|
|
34
|
+
qname = Webhookdb::DBAdapter::PG.new.escape_identifier(name)
|
|
35
|
+
organization.admin_connection do |conn|
|
|
36
|
+
conn << "CREATE OR REPLACE VIEW #{qname} AS (#{sql})"
|
|
37
|
+
end
|
|
38
|
+
return sv
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def before_destroy
|
|
43
|
+
raise Webhookdb::InvariantViolation, "#{self.inspect} name became invalid somehow" unless
|
|
44
|
+
Webhookdb::DBAdapter.valid_identifier?(self.name)
|
|
45
|
+
if self.organization.admin_connection_url_raw.present?
|
|
46
|
+
qname = Webhookdb::DBAdapter::PG.new.escape_identifier(self.name)
|
|
47
|
+
self.organization.admin_connection do |conn|
|
|
48
|
+
conn << "DROP VIEW IF EXISTS #{qname}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Table: saved_views
|
|
56
|
+
# ----------------------------------------------------------------------------------------------------
|
|
57
|
+
# Columns:
|
|
58
|
+
# id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
|
|
59
|
+
# created_at | timestamp with time zone | NOT NULL DEFAULT now()
|
|
60
|
+
# updated_at | timestamp with time zone |
|
|
61
|
+
# organization_id | integer | NOT NULL
|
|
62
|
+
# name | text | NOT NULL
|
|
63
|
+
# sql | text | NOT NULL
|
|
64
|
+
# created_by_id | integer |
|
|
65
|
+
# text_search | tsvector |
|
|
66
|
+
# Indexes:
|
|
67
|
+
# saved_views_pkey | PRIMARY KEY btree (id)
|
|
68
|
+
# saved_views_organization_id_name_key | UNIQUE btree (organization_id, name)
|
|
69
|
+
# saved_views_organization_id_index | btree (organization_id)
|
|
70
|
+
# Foreign key constraints:
|
|
71
|
+
# saved_views_created_by_id_fkey | (created_by_id) REFERENCES customers(id) ON DELETE SET NULL
|
|
72
|
+
# saved_views_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
73
|
+
# ----------------------------------------------------------------------------------------------------
|
data/lib/webhookdb/sentry.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Webhookdb::Sentry
|
|
|
12
12
|
|
|
13
13
|
configurable(:sentry) do
|
|
14
14
|
setting :dsn, ""
|
|
15
|
+
setting :log_level, :warn
|
|
15
16
|
|
|
16
17
|
# Apply the current configuration to Sentry.
|
|
17
18
|
# See https://docs.sentry.io/clients/ruby/config/ for more info.
|
|
@@ -22,6 +23,7 @@ module Webhookdb::Sentry
|
|
|
22
23
|
Sentry.init do |config|
|
|
23
24
|
config.dsn = dsn
|
|
24
25
|
config.logger = self.logger
|
|
26
|
+
config.logger.level = self.log_level
|
|
25
27
|
end
|
|
26
28
|
else
|
|
27
29
|
Sentry.instance_variable_set(:@main_hub, nil)
|
|
@@ -142,6 +142,11 @@ module Webhookdb::Service::Helpers
|
|
|
142
142
|
merror!(403, message, code: "permission_check")
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
+
def check_feature_access!(org, role)
|
|
146
|
+
return if org.feature_roles.include?(role)
|
|
147
|
+
permission_error!("This feature is not enabled for your organization.")
|
|
148
|
+
end
|
|
149
|
+
|
|
145
150
|
# Raise a 400 error for unstructured validation.
|
|
146
151
|
# @param errors [Array<String>,String] Error messages, like 'password is invalid'.
|
|
147
152
|
# @param message [String] If not given, build it from the errors list.
|
|
@@ -38,6 +38,14 @@ module Webhookdb::Service::Middleware
|
|
|
38
38
|
credentials: false,
|
|
39
39
|
expose: "*"
|
|
40
40
|
end
|
|
41
|
+
allow do
|
|
42
|
+
origins("*")
|
|
43
|
+
resource "/v1/db/run_sql",
|
|
44
|
+
headers: :any,
|
|
45
|
+
methods: [:get, :post],
|
|
46
|
+
credentials: false,
|
|
47
|
+
expose: "*"
|
|
48
|
+
end
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
|
|
@@ -45,6 +53,7 @@ module Webhookdb::Service::Middleware
|
|
|
45
53
|
builder.use(Rack::ContentLength)
|
|
46
54
|
builder.use(Rack::Chunked)
|
|
47
55
|
builder.use(Sentry::Rack::CaptureExceptions)
|
|
56
|
+
builder.use(Rack::Deflater)
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def self.add_dev_middleware(builder)
|
|
@@ -7,18 +7,20 @@ module Webhookdb::Service::Types
|
|
|
7
7
|
ctx.const_set(:NormalizedEmail, NormalizedEmail)
|
|
8
8
|
ctx.const_set(:NormalizedPhone, NormalizedPhone)
|
|
9
9
|
ctx.const_set(:CommaSepArray, CommaSepArray)
|
|
10
|
+
ctx.const_set(:TrimmedString, TrimmedString)
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
class NormalizedEmail
|
|
13
|
-
def self.parse(value)
|
|
14
|
-
return value.downcase.strip
|
|
15
|
-
end
|
|
13
|
+
class NormalizedEmail < String
|
|
14
|
+
def self.parse(value) = self.new(value.downcase.strip)
|
|
16
15
|
end
|
|
17
16
|
|
|
18
|
-
class NormalizedPhone
|
|
19
|
-
def self.parse(value)
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
class NormalizedPhone < String
|
|
18
|
+
def self.parse(value) = self.new(Webhookdb::PhoneNumber::US.normalize(value))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class TrimmedString < String
|
|
22
|
+
def self.parse(value) = self.new(value.strip)
|
|
23
|
+
def self.map(arr) = arr.map { |a| self.new(a) }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
class CommaSepArray
|
|
@@ -20,8 +20,7 @@ module Webhookdb::Service::Validators
|
|
|
20
20
|
def validate_param!(attr_name, params)
|
|
21
21
|
val = params[attr_name]
|
|
22
22
|
return if val.blank? && @allow_blank
|
|
23
|
-
|
|
24
|
-
return if re.match?(val)
|
|
23
|
+
return if Webhookdb::DBAdapter.valid_identifier?(val)
|
|
25
24
|
raise Grape::Exceptions::Validation.new(
|
|
26
25
|
params: [@scope.full_name(attr_name)],
|
|
27
26
|
message: "is not a valid database identifier for WebhookDB. " +
|
|
@@ -23,7 +23,7 @@ module Webhookdb::Service::ViewApi
|
|
|
23
23
|
|
|
|
24
24
|
tmpl_file = File.open(Webhookdb::DATA_DIR + data_rel_path)
|
|
25
25
|
liquid_tmpl = Liquid::Template.parse(tmpl_file.read)
|
|
26
|
-
rendered = liquid_tmpl.render!(vars.
|
|
26
|
+
rendered = liquid_tmpl.render!(vars.deep_stringify_keys, registers: {})
|
|
27
27
|
_endpoint.content_type content_type
|
|
28
28
|
if serialize_view_params
|
|
29
29
|
_endpoint.cookies[:whdbviewparams] = {path: data_rel_path, vars:, content_type:}.to_json
|
|
@@ -20,6 +20,7 @@ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integr
|
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
22
|
plugin :timestamps
|
|
23
|
+
plugin :text_searchable, terms: [:service_name, :table_name, :organization]
|
|
23
24
|
plugin :column_encryption do |enc|
|
|
24
25
|
enc.column :data_encryption_secret
|
|
25
26
|
enc.column :webhook_secret
|
|
@@ -145,10 +146,12 @@ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integr
|
|
|
145
146
|
def destroy_self_and_all_dependents
|
|
146
147
|
self.dependents.each(&:destroy_self_and_all_dependents)
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
if self.organization.admin_connection_url.present?
|
|
150
|
+
begin
|
|
151
|
+
self.replicator.admin_dataset(timeout: :fast) { |ds| ds.db << "DROP TABLE #{self.table_name}" }
|
|
152
|
+
rescue Sequel::DatabaseError => e
|
|
153
|
+
raise e unless e.wrapped_exception.is_a?(PG::UndefinedTable)
|
|
154
|
+
end
|
|
152
155
|
end
|
|
153
156
|
self.destroy
|
|
154
157
|
end
|
|
@@ -222,11 +225,7 @@ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integr
|
|
|
222
225
|
|
|
223
226
|
def rename_table(to:)
|
|
224
227
|
Webhookdb::Organization::DatabaseMigration.guard_ongoing!(self.organization)
|
|
225
|
-
|
|
226
|
-
msg = "Sorry, this is not a valid table name. " + Webhookdb::DBAdapter::INVALID_IDENTIFIER_MESSAGE
|
|
227
|
-
msg += " And we see you what you did there ;)" if to.include?(";") && to.downcase.include?("drop")
|
|
228
|
-
raise TableRenameError, msg
|
|
229
|
-
end
|
|
228
|
+
Webhookdb::DBAdapter.validate_identifier!(to, type: "table")
|
|
230
229
|
self.db.transaction do
|
|
231
230
|
begin
|
|
232
231
|
self.organization.admin_connection { |db| db << "ALTER TABLE #{self.table_name} RENAME TO #{to}" }
|
|
@@ -333,7 +332,7 @@ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integr
|
|
|
333
332
|
end
|
|
334
333
|
|
|
335
334
|
# Table: service_integrations
|
|
336
|
-
#
|
|
335
|
+
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
337
336
|
# Columns:
|
|
338
337
|
# id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
|
|
339
338
|
# created_at | timestamp with time zone | NOT NULL DEFAULT now()
|
|
@@ -350,6 +349,8 @@ end
|
|
|
350
349
|
# depends_on_id | integer |
|
|
351
350
|
# data_encryption_secret | text |
|
|
352
351
|
# skip_webhook_verification | boolean | NOT NULL DEFAULT false
|
|
352
|
+
# webhookdb_api_key | text |
|
|
353
|
+
# text_search | tsvector |
|
|
353
354
|
# Indexes:
|
|
354
355
|
# service_integrations_pkey | PRIMARY KEY btree (id)
|
|
355
356
|
# service_integrations_opaque_id_key | UNIQUE btree (opaque_id)
|
|
@@ -358,8 +359,9 @@ end
|
|
|
358
359
|
# service_integrations_depends_on_id_fkey | (depends_on_id) REFERENCES service_integrations(id) ON DELETE RESTRICT
|
|
359
360
|
# service_integrations_organization_id_fkey | (organization_id) REFERENCES organizations(id)
|
|
360
361
|
# Referenced By:
|
|
361
|
-
# backfill_jobs
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
362
|
+
# backfill_jobs | backfill_jobs_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id) ON DELETE CASCADE
|
|
363
|
+
# backfill_job_service_integration_locks | backfill_job_service_integration_lo_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id) ON DELETE CASCADE
|
|
364
|
+
# service_integrations | service_integrations_depends_on_id_fkey | (depends_on_id) REFERENCES service_integrations(id) ON DELETE RESTRICT
|
|
365
|
+
# sync_targets | sync_targets_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id) ON DELETE CASCADE
|
|
366
|
+
# webhook_subscriptions | webhook_subscriptions_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id)
|
|
367
|
+
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -45,7 +45,7 @@ RSpec.shared_examples "a replicator" do |name|
|
|
|
45
45
|
if expected_row
|
|
46
46
|
expect(ds.first).to match(expected_row)
|
|
47
47
|
else
|
|
48
|
-
expect(ds.first[:data]).to eq(expected_data)
|
|
48
|
+
expect(ds.first[:data].to_h).to eq(expected_data)
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -59,7 +59,7 @@ RSpec.shared_examples "a replicator" do |name|
|
|
|
59
59
|
if expected_row
|
|
60
60
|
expect(ds.first).to match(expected_row)
|
|
61
61
|
else
|
|
62
|
-
expect(ds.first[:data]).to eq(expected_data)
|
|
62
|
+
expect(ds.first[:data].to_h).to eq(expected_data)
|
|
63
63
|
end
|
|
64
64
|
# this is how a fully qualified table is represented (schema->table, table->column)
|
|
65
65
|
expect(ds.opts[:from].first).to have_attributes(table: "xyz", column: svc.service_integration.table_name.to_sym)
|
|
@@ -277,7 +277,7 @@ RSpec.shared_examples "a replicator that prevents overwriting new data with old"
|
|
|
277
277
|
if expected_old_row
|
|
278
278
|
expect(ds.first).to match(expected_old_row)
|
|
279
279
|
else
|
|
280
|
-
expect(ds.first[:data]).to eq(expected_old_data)
|
|
280
|
+
expect(ds.first[:data].to_h).to eq(expected_old_data)
|
|
281
281
|
end
|
|
282
282
|
|
|
283
283
|
upsert_webhook(svc, body: new_body)
|
|
@@ -285,7 +285,7 @@ RSpec.shared_examples "a replicator that prevents overwriting new data with old"
|
|
|
285
285
|
if expected_new_row
|
|
286
286
|
expect(ds.first).to match(expected_new_row)
|
|
287
287
|
else
|
|
288
|
-
expect(ds.first[:data]).to eq(expected_new_data)
|
|
288
|
+
expect(ds.first[:data].to_h).to eq(expected_new_data)
|
|
289
289
|
end
|
|
290
290
|
end
|
|
291
291
|
end
|
|
@@ -299,7 +299,7 @@ RSpec.shared_examples "a replicator that prevents overwriting new data with old"
|
|
|
299
299
|
if expected_new_row
|
|
300
300
|
expect(ds.first).to match(expected_new_row)
|
|
301
301
|
else
|
|
302
|
-
expect(ds.first[:data]).to eq(expected_new_data)
|
|
302
|
+
expect(ds.first[:data].to_h).to eq(expected_new_data)
|
|
303
303
|
end
|
|
304
304
|
|
|
305
305
|
upsert_webhook(svc, body: old_body)
|
|
@@ -307,7 +307,7 @@ RSpec.shared_examples "a replicator that prevents overwriting new data with old"
|
|
|
307
307
|
if expected_new_row
|
|
308
308
|
expect(ds.first).to match(expected_new_row)
|
|
309
309
|
else
|
|
310
|
-
expect(ds.first[:data]).to eq(expected_new_data)
|
|
310
|
+
expect(ds.first[:data].to_h).to eq(expected_new_data)
|
|
311
311
|
end
|
|
312
312
|
end
|
|
313
313
|
end
|
|
@@ -475,7 +475,7 @@ RSpec.shared_examples "a replicator that deals with resources and wrapped events
|
|
|
475
475
|
upsert_webhook(svc, body: resource_json, headers: resource_headers)
|
|
476
476
|
svc.readonly_dataset do |ds|
|
|
477
477
|
expect(ds.all).to have_length(1)
|
|
478
|
-
expect(ds.first[:data]).to eq(resource_json)
|
|
478
|
+
expect(ds.first[:data].to_h).to eq(resource_json)
|
|
479
479
|
end
|
|
480
480
|
end
|
|
481
481
|
|
|
@@ -484,7 +484,7 @@ RSpec.shared_examples "a replicator that deals with resources and wrapped events
|
|
|
484
484
|
upsert_webhook(svc, body: resource_in_envelope_json, headers: resource_in_envelope_headers)
|
|
485
485
|
svc.readonly_dataset do |ds|
|
|
486
486
|
expect(ds.all).to have_length(1)
|
|
487
|
-
expect(ds.first[:data]).to eq(resource_json)
|
|
487
|
+
expect(ds.first[:data].to_h).to eq(resource_json)
|
|
488
488
|
end
|
|
489
489
|
end
|
|
490
490
|
end
|
|
@@ -104,13 +104,14 @@ module Webhookdb::SpecHelpers::Whdb
|
|
|
104
104
|
this.let(:request_method) { nil }
|
|
105
105
|
this.let(:request_body) { nil }
|
|
106
106
|
this.let(:request_headers) { nil }
|
|
107
|
+
this.let(:rack_request) { nil }
|
|
107
108
|
this.let(:webhook_request) do
|
|
108
109
|
Webhookdb::Replicator::WebhookRequest.new(
|
|
109
|
-
body: request_body, method: request_method, path: request_path, headers: request_headers,
|
|
110
|
+
body: request_body, method: request_method, path: request_path, headers: request_headers, rack_request:,
|
|
110
111
|
)
|
|
111
112
|
end
|
|
112
113
|
this.define_method(:upsert_webhook) do |svc, **kw|
|
|
113
|
-
params = {body: request_body, headers: request_headers, method: request_method, path: request_path}
|
|
114
|
+
params = {body: request_body, headers: request_headers, method: request_method, path: request_path, rack_request:}
|
|
114
115
|
params.merge!(**kw)
|
|
115
116
|
svc.upsert_webhook(Webhookdb::Replicator::WebhookRequest.new(**params))
|
|
116
117
|
end
|
|
@@ -23,6 +23,7 @@ class Webhookdb::Subscription < Webhookdb::Postgres::Model(:subscriptions)
|
|
|
23
23
|
|
|
24
24
|
plugin :timestamps
|
|
25
25
|
plugin :soft_deletes
|
|
26
|
+
plugin :text_searchable, terms: [:organization]
|
|
26
27
|
|
|
27
28
|
configurable(:subscription) do
|
|
28
29
|
setting :billing_enabled, false
|
|
@@ -196,6 +197,7 @@ end
|
|
|
196
197
|
# stripe_id | text | NOT NULL
|
|
197
198
|
# stripe_customer_id | text | NOT NULL DEFAULT ''::text
|
|
198
199
|
# stripe_json | jsonb | DEFAULT '{}'::jsonb
|
|
200
|
+
# text_search | tsvector |
|
|
199
201
|
# Indexes:
|
|
200
202
|
# subscriptions_pkey | PRIMARY KEY btree (id)
|
|
201
203
|
# subscriptions_stripe_id_key | UNIQUE btree (stripe_id)
|
|
@@ -74,6 +74,7 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
plugin :timestamps
|
|
77
|
+
plugin :text_searchable, terms: [:service_integration, :created_by]
|
|
77
78
|
plugin :column_encryption do |enc|
|
|
78
79
|
enc.column :connection_url
|
|
79
80
|
end
|
|
@@ -327,7 +328,8 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
|
327
328
|
# it wasn't in the last sync; however that is likely not a big problem
|
|
328
329
|
# since clients need to handle updates in any case.
|
|
329
330
|
def dataset_to_sync
|
|
330
|
-
|
|
331
|
+
# Use admin dataset, since the client could be using all their readonly conns.
|
|
332
|
+
@replicator.admin_dataset do |ds|
|
|
331
333
|
# Find rows updated before we started
|
|
332
334
|
tscond = (@timestamp_expr <= @now)
|
|
333
335
|
# Find rows updated after the last sync was run
|
|
@@ -364,7 +366,12 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
|
364
366
|
# This is handled well so no need to re-raise.
|
|
365
367
|
# We already committed the last page that was successful,
|
|
366
368
|
# so we can just stop syncing at this point to try again later.
|
|
367
|
-
|
|
369
|
+
|
|
370
|
+
# Don't spam our logs with downstream errors
|
|
371
|
+
idem_key = "sync_target_http_error-#{self.sync_target.id}-#{e.class.name}"
|
|
372
|
+
Webhookdb::Idempotency.every(1.hour).in_memory.under_key(idem_key) do
|
|
373
|
+
self.sync_target.logger.warn("sync_target_http_error", error: e)
|
|
374
|
+
end
|
|
368
375
|
end
|
|
369
376
|
|
|
370
377
|
def _flush_http_chunk(chunk)
|
|
@@ -480,6 +487,7 @@ end
|
|
|
480
487
|
# last_synced_at | timestamp with time zone |
|
|
481
488
|
# last_applied_schema | text | NOT NULL DEFAULT ''::text
|
|
482
489
|
# page_size | integer | NOT NULL
|
|
490
|
+
# text_search | tsvector |
|
|
483
491
|
# Indexes:
|
|
484
492
|
# sync_targets_pkey | PRIMARY KEY btree (id)
|
|
485
493
|
# sync_targets_opaque_id_key | UNIQUE btree (opaque_id)
|
|
@@ -28,7 +28,9 @@ module Webhookdb::Tasks
|
|
|
28
28
|
SemanticLogger.add_appender(io: feedback_io)
|
|
29
29
|
|
|
30
30
|
commit = Webhookdb::RACK_ENV != "test"
|
|
31
|
-
|
|
31
|
+
clsname = template_class_name.classify
|
|
32
|
+
(clsname += "s") if template_class_name.end_with?("s") && !clsname.end_with?("s")
|
|
33
|
+
delivery = Webhookdb::Message::Delivery.preview(clsname, commit:)
|
|
32
34
|
feedback_io << "*** Created MessageDelivery: #{delivery.values}\n\n"
|
|
33
35
|
feedback_io << delivery.body_with_mediatype!("text/plain")&.content
|
|
34
36
|
feedback_io << "\n\n"
|
data/lib/webhookdb/version.rb
CHANGED