webhookdb 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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