webhookdb 1.1.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/db/migrations/039_saved_query.rb +17 -0
- data/lib/webhookdb/api/db.rb +2 -19
- data/lib/webhookdb/api/helpers.rb +27 -0
- data/lib/webhookdb/api/saved_queries.rb +219 -0
- data/lib/webhookdb/api/sync_targets.rb +2 -6
- data/lib/webhookdb/api/webhook_subscriptions.rb +12 -18
- data/lib/webhookdb/api.rb +11 -2
- data/lib/webhookdb/apps.rb +2 -0
- data/lib/webhookdb/email_octopus.rb +2 -0
- data/lib/webhookdb/envfixer.rb +29 -0
- data/lib/webhookdb/fixtures/saved_queries.rb +27 -0
- data/lib/webhookdb/google_calendar.rb +6 -0
- data/lib/webhookdb/icalendar.rb +6 -0
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +7 -1
- data/lib/webhookdb/jobs/prepare_database_connections.rb +2 -2
- data/lib/webhookdb/jobs/scheduled_backfills.rb +14 -13
- data/lib/webhookdb/organization.rb +8 -2
- data/lib/webhookdb/postgres.rb +3 -0
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +1 -1
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +6 -3
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +3 -1
- data/lib/webhookdb/replicator/twilio_sms_v1.rb +3 -1
- data/lib/webhookdb/saved_query.rb +28 -0
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb.rb +24 -40
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 449e2e3d37eaf61467720f1360355b427f71032cfbf3322165be61b8b01922ea
|
4
|
+
data.tar.gz: 045152c0ea6fce9ce154abc59ba149c97a9b9ab9e85da3ac007fa2e6db88ec43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b33b003d0f25c220f3092bebbf0fe97f594b0f4bc6870395a96d169e990a02321f3a78a4b38491589866375b0846ce38d61b4ce779f05cb709f3801f4563f2b
|
7
|
+
data.tar.gz: c61e78f300e587f096de14c34f8d6c0ca14dc32006ae79a9fdf5a2bc79f0be0fc868c351f28b57f0c49eb4f183fe933e70373ac0a5b99868eb7e9a76b64127c8
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Sequel.migration do
|
4
|
+
change do
|
5
|
+
create_table(:saved_queries) do
|
6
|
+
primary_key :id
|
7
|
+
timestamptz :created_at, null: false, default: Sequel.function(:now)
|
8
|
+
timestamptz :updated_at
|
9
|
+
foreign_key :organization_id, :organizations, null: false, unique: true, on_delete: :cascade
|
10
|
+
foreign_key :created_by_id, :customers, on_delete: :set_null
|
11
|
+
text :opaque_id, null: false, unique: true
|
12
|
+
text :description, null: false
|
13
|
+
text :sql, null: false
|
14
|
+
boolean :public, null: false, default: false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/webhookdb/api/db.rb
CHANGED
@@ -124,25 +124,8 @@ class Webhookdb::API::Db < Webhookdb::API::V1
|
|
124
124
|
end
|
125
125
|
post :sql do
|
126
126
|
org = lookup_org!(allow_connstr_auth: true)
|
127
|
-
|
128
|
-
|
129
|
-
rescue Sequel::DatabaseError => e
|
130
|
-
self.logger.error("db_query_database_error", error: e)
|
131
|
-
# We want to handle InsufficientPrivileges and UndefinedTable explicitly
|
132
|
-
# since we can hint the user at what to do.
|
133
|
-
# Otherwise, we should just return the Postgres exception.
|
134
|
-
case e.wrapped_exception
|
135
|
-
when PG::UndefinedTable
|
136
|
-
missing_table = e.wrapped_exception.message.match(/relation (.+) does not/)&.captures&.first
|
137
|
-
msg = "The table #{missing_table} does not exist. Run `webhookdb db tables` to see available tables." if
|
138
|
-
missing_table
|
139
|
-
when PG::InsufficientPrivilege
|
140
|
-
msg = "You do not have permission to perform this query. Queries must be read-only."
|
141
|
-
else
|
142
|
-
msg = e.wrapped_exception.message
|
143
|
-
end
|
144
|
-
merror!(403, msg, code: "invalid_query")
|
145
|
-
end
|
127
|
+
r, msg = execute_readonly_query(org, params[:query])
|
128
|
+
merror!(403, msg, code: "invalid_query") if r.nil?
|
146
129
|
status 200
|
147
130
|
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached})
|
148
131
|
end
|
@@ -250,4 +250,31 @@ module Webhookdb::API::Helpers
|
|
250
250
|
service_integration_opaque_id: opaque_id,
|
251
251
|
)
|
252
252
|
end
|
253
|
+
|
254
|
+
# Run the given SQL inside the org, and use special error handling if it fails.
|
255
|
+
# @return [Array<Webhookdb::Organization::QueryResult,String,nil>] Tuple of query result, and optional message.
|
256
|
+
# On query success, return <QueryResult, nil>.
|
257
|
+
# On DatabaseError, return <nil, message>.
|
258
|
+
# On other types of errors, raise.
|
259
|
+
def execute_readonly_query(org, sql)
|
260
|
+
result = org.execute_readonly_query(sql)
|
261
|
+
return result, nil
|
262
|
+
rescue Sequel::DatabaseError => e
|
263
|
+
self.logger.error("db_query_database_error", error: e)
|
264
|
+
# We want to handle InsufficientPrivileges and UndefinedTable explicitly
|
265
|
+
# since we can hint the user at what to do.
|
266
|
+
# Otherwise, we should just return the Postgres exception.
|
267
|
+
msg = ""
|
268
|
+
case e.wrapped_exception
|
269
|
+
when PG::UndefinedTable
|
270
|
+
missing_table = e.wrapped_exception.message.match(/relation (.+) does not/)&.captures&.first
|
271
|
+
msg = "The table #{missing_table} does not exist. Run `webhookdb db tables` to see available tables." if
|
272
|
+
missing_table
|
273
|
+
when PG::InsufficientPrivilege
|
274
|
+
msg = "You do not have permission to perform this query. Queries must be read-only."
|
275
|
+
else
|
276
|
+
msg = e.wrapped_exception.message
|
277
|
+
end
|
278
|
+
return [nil, msg]
|
279
|
+
end
|
253
280
|
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webhookdb/api"
|
4
|
+
require "webhookdb/saved_query"
|
5
|
+
|
6
|
+
class Webhookdb::API::SavedQueries < Webhookdb::API::V1
|
7
|
+
resource :organizations do
|
8
|
+
route_param :org_identifier do
|
9
|
+
resource :saved_queries do
|
10
|
+
helpers do
|
11
|
+
def lookup!
|
12
|
+
org = lookup_org!
|
13
|
+
# We can add other identifiers in the future
|
14
|
+
cq = org.saved_queries_dataset[opaque_id: params[:query_identifier]]
|
15
|
+
merror!(403, "There is no saved query with that identifier.") if cq.nil?
|
16
|
+
return cq
|
17
|
+
end
|
18
|
+
|
19
|
+
def guard_editable!(customer, cq)
|
20
|
+
return if customer === cq.created_by
|
21
|
+
return if has_admin?(cq.organization, customer:)
|
22
|
+
permission_error!("You must be the query's creator or an org admin.")
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute_readonly_query_with_suggestion(org, sql)
|
26
|
+
r, message = execute_readonly_query(org, sql)
|
27
|
+
return r, nil unless r.nil?
|
28
|
+
msg = "Something went wrong running your query. Perhaps a table it depends on was deleted. " \
|
29
|
+
"Check out #{Webhookdb::SavedQuery::DOCS_URL} for troubleshooting tips. " \
|
30
|
+
"Here's what went wrong: #{message}"
|
31
|
+
return r, msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Returns a list of all custom queries associated with the org."
|
36
|
+
get do
|
37
|
+
queries = lookup_org!.saved_queries
|
38
|
+
message = ""
|
39
|
+
if queries.empty?
|
40
|
+
message = "This organization doesn't have any saved queries yet.\n" \
|
41
|
+
"Use `webhookdb saved-query create` to set one up."
|
42
|
+
end
|
43
|
+
present_collection queries, with: SavedQueryEntity, message:
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Creates a custom query."
|
47
|
+
params do
|
48
|
+
optional :description, type: String, prompt: "What is the query used for? "
|
49
|
+
optional :sql, type: String, prompt: "Enter the SQL you would like to run: "
|
50
|
+
optional :public, type: Boolean
|
51
|
+
end
|
52
|
+
post :create do
|
53
|
+
cust = current_customer
|
54
|
+
org = lookup_org!
|
55
|
+
_, errmsg = execute_readonly_query_with_suggestion(org, params[:sql])
|
56
|
+
if errmsg
|
57
|
+
Webhookdb::API::Helpers.prompt_for_required_param!(
|
58
|
+
request,
|
59
|
+
:sql,
|
60
|
+
"Enter a new query:",
|
61
|
+
output: "That query was invalid. #{errmsg}\n" \
|
62
|
+
"You can iterate on your query by connecting to your database from any SQL editor.\n" \
|
63
|
+
"Use `webhookdb db connection` to get your query string.",
|
64
|
+
)
|
65
|
+
end
|
66
|
+
cq = Webhookdb::SavedQuery.create(
|
67
|
+
description: params[:description],
|
68
|
+
sql: params[:sql],
|
69
|
+
organization: org,
|
70
|
+
created_by: cust,
|
71
|
+
public: params[:public] || false,
|
72
|
+
)
|
73
|
+
message = "You have created a new saved query with an id of '#{cq.opaque_id}'. " \
|
74
|
+
"You can run it through the CLI, or through the API with or without authentication. " \
|
75
|
+
"See #{Webhookdb::SavedQuery::DOCS_URL} for more information."
|
76
|
+
status 200
|
77
|
+
present cq, with: SavedQueryEntity, message:
|
78
|
+
end
|
79
|
+
|
80
|
+
route_param :query_identifier, type: String do
|
81
|
+
desc "Returns the query with the given identifier."
|
82
|
+
get do
|
83
|
+
cq = lookup!
|
84
|
+
status 200
|
85
|
+
message = "See #{Webhookdb::SavedQuery::DOCS_URL} to see how to run or modify your query."
|
86
|
+
present cq, with: SavedQueryEntity, message:
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "Runs the query with the given identifier."
|
90
|
+
get :run do
|
91
|
+
_customer = current_customer
|
92
|
+
org = lookup_org!
|
93
|
+
cq = lookup!
|
94
|
+
r, msg = execute_readonly_query_with_suggestion(org, cq.sql)
|
95
|
+
merror!(400, msg) if r.nil?
|
96
|
+
status 200
|
97
|
+
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached})
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "Updates the field on a custom query."
|
101
|
+
params do
|
102
|
+
optional :field, type: String, prompt: "What field would you like to update (one of: " \
|
103
|
+
"#{Webhookdb::SavedQuery::CLI_EDITABLE_FIELDS.join(', ')}): "
|
104
|
+
optional :value, type: String, prompt: "What is the new value? "
|
105
|
+
end
|
106
|
+
post :update do
|
107
|
+
customer = current_customer
|
108
|
+
cq = lookup!
|
109
|
+
guard_editable!(customer, cq)
|
110
|
+
# Instead of specifying which values are valid for the optional `field` param in the param declaration,
|
111
|
+
# we do the validation here so that we can provide a more helpful error message
|
112
|
+
unless Webhookdb::SavedQuery::CLI_EDITABLE_FIELDS.include?(params[:field])
|
113
|
+
merror!(400, "That field is not editable.")
|
114
|
+
end
|
115
|
+
value = params[:value]
|
116
|
+
case params[:field]
|
117
|
+
when "public"
|
118
|
+
begin
|
119
|
+
value = Webhookdb.parse_bool(value)
|
120
|
+
rescue ArgumentError => e
|
121
|
+
Webhookdb::API::Helpers.prompt_for_required_param!(
|
122
|
+
request,
|
123
|
+
:value,
|
124
|
+
e.message + "\nAny boolean-like string (true, false, yes, no, etc) will work:",
|
125
|
+
)
|
126
|
+
end
|
127
|
+
cq.public = value
|
128
|
+
when "sql"
|
129
|
+
r, msg = execute_readonly_query_with_suggestion(cq.organization, value)
|
130
|
+
if r.nil?
|
131
|
+
Webhookdb::API::Helpers.prompt_for_required_param!(
|
132
|
+
request,
|
133
|
+
:value,
|
134
|
+
"Enter your query:",
|
135
|
+
output: msg,
|
136
|
+
)
|
137
|
+
end
|
138
|
+
cq.sql = value
|
139
|
+
else
|
140
|
+
cq.send(:"#{params[:field]}=", value)
|
141
|
+
end
|
142
|
+
cq.save_changes
|
143
|
+
status 200
|
144
|
+
# Do not render the value here, it can be very long.
|
145
|
+
message = "You have updated '#{params[:field]}' on saved query '#{cq.opaque_id}'."
|
146
|
+
present cq, with: SavedQueryEntity, message:
|
147
|
+
end
|
148
|
+
|
149
|
+
params do
|
150
|
+
optional :field, type: String, values: Webhookdb::SavedQuery::INFO_FIELDS.keys + [""]
|
151
|
+
end
|
152
|
+
post :info do
|
153
|
+
cq = lookup!
|
154
|
+
data = Webhookdb::SavedQuery::INFO_FIELDS.
|
155
|
+
to_h { |k, v| [k.to_sym, cq.send(v)] }
|
156
|
+
|
157
|
+
field_name = params[:field]
|
158
|
+
blocks = Webhookdb::Formatting.blocks
|
159
|
+
if field_name.present?
|
160
|
+
blocks.line(data.fetch(field_name.to_sym))
|
161
|
+
else
|
162
|
+
rows = data.map do |k, v|
|
163
|
+
[k.to_s.humanize, v.to_s]
|
164
|
+
end
|
165
|
+
blocks.table(["Field", "Value"], rows)
|
166
|
+
end
|
167
|
+
r = {blocks: blocks.as_json}
|
168
|
+
status 200
|
169
|
+
present r
|
170
|
+
end
|
171
|
+
|
172
|
+
post :delete do
|
173
|
+
customer = current_customer
|
174
|
+
cq = lookup!
|
175
|
+
guard_editable!(customer, cq)
|
176
|
+
cq.destroy
|
177
|
+
status 200
|
178
|
+
present cq, with: SavedQueryEntity,
|
179
|
+
message: "You have successfully deleted the saved query '#{cq.description}'."
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
resource :saved_queries do
|
187
|
+
route_param :query_identifier, type: String do
|
188
|
+
get :run do
|
189
|
+
# This endpoint can be used publicly, so should expose as little information as possible.
|
190
|
+
# Do not expose permissions or query details.
|
191
|
+
cq = Webhookdb::SavedQuery[opaque_id: params[:query_identifier]]
|
192
|
+
forbidden! if cq.nil?
|
193
|
+
if cq.private?
|
194
|
+
authed = Webhookdb::API::ConnstrAuth.find_authed([cq.organization], request)
|
195
|
+
if !authed && (cust = current_customer?)
|
196
|
+
authed = !cust.verified_memberships_dataset.where(organization: cq.organization).empty?
|
197
|
+
end
|
198
|
+
forbidden! unless authed
|
199
|
+
end
|
200
|
+
r, _ = execute_readonly_query(cq.organization, cq.sql)
|
201
|
+
merror!(400, "Something went wrong running the query.") if r.nil?
|
202
|
+
status 200
|
203
|
+
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached})
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class SavedQueryEntity < Webhookdb::API::BaseEntity
|
209
|
+
expose :opaque_id, as: :id
|
210
|
+
expose :description
|
211
|
+
expose :sql
|
212
|
+
expose :public
|
213
|
+
expose :run_url
|
214
|
+
|
215
|
+
def self.display_headers
|
216
|
+
return [[:id, "Id"], [:description, "Description"], [:public, "Public"], [:run_url, "Run URL"]]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -112,18 +112,14 @@ class Webhookdb::API::SyncTargets < Webhookdb::API::V1
|
|
112
112
|
params do
|
113
113
|
use :connection_url
|
114
114
|
use :sync_target_params
|
115
|
-
|
116
|
-
type: String, allow_blank: false,
|
117
|
-
desc: "This is a deprecated parameter. In the future, please use `service_integration_identifier`."
|
118
|
-
optional :service_integration_identifier, type: String, allow_blank: false
|
119
|
-
at_least_one_of :service_integration_opaque_id, :service_integration_identifier
|
115
|
+
requires :service_integration_identifier, type: String, allow_blank: false
|
120
116
|
end
|
121
117
|
route_setting :target_type, target_type_resource
|
122
118
|
post :create do
|
123
119
|
customer = current_customer
|
124
120
|
org = lookup_org!(customer:)
|
125
121
|
ensure_admin!(org, customer:)
|
126
|
-
identifier = params[:service_integration_identifier]
|
122
|
+
identifier = params[:service_integration_identifier]
|
127
123
|
sint = lookup_service_integration!(org, identifier)
|
128
124
|
|
129
125
|
validate_period!(sint.organization, params[:period_seconds])
|
@@ -6,46 +6,40 @@ class Webhookdb::API::WebhookSubscriptions < Webhookdb::API::V1
|
|
6
6
|
resource :organizations do
|
7
7
|
route_param :org_identifier, type: String do
|
8
8
|
resource :webhook_subscriptions do
|
9
|
-
desc "Return all
|
9
|
+
desc "Return all notifications for the given org and its integrations."
|
10
10
|
get do
|
11
11
|
org = lookup_org!
|
12
12
|
subs = org.all_webhook_subscriptions
|
13
13
|
message = ""
|
14
14
|
if subs.empty?
|
15
15
|
message = "Organization #{org.name} has no webhook subscriptions set up.\n" \
|
16
|
-
"Use `webhookdb
|
16
|
+
"Use `webhookdb notifications create` to set one up."
|
17
17
|
end
|
18
18
|
status 200
|
19
19
|
present_collection subs, with: Webhookdb::API::WebhookSubscriptionEntity, message:
|
20
20
|
end
|
21
21
|
|
22
22
|
params do
|
23
|
-
optional :url, prompt: "Enter the URL that WebhookDB should POST webhooks to:"
|
24
|
-
optional :webhook_secret, prompt: "Enter a random secret used to sign and verify webhooks to the given url:"
|
25
23
|
optional :service_integration_identifier,
|
26
24
|
type: String,
|
27
|
-
desc: "If provided, attach the webhook subscription to this integration rather than the org."
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
desc: "If provided, attach the webhook subscription to this integration rather than the org.",
|
26
|
+
prompt: "Which integration is this for? Use the service name, table name, or opaque id.\n" \
|
27
|
+
"See your integrations with `webhookdb integrations list`:"
|
28
|
+
optional :url, prompt: "Enter the URL that WebhookDB should POST notifications to:"
|
29
|
+
optional :webhook_secret,
|
30
|
+
prompt: "Enter a random secret used to sign and verify notifications to the given url:"
|
31
31
|
end
|
32
32
|
post :create do
|
33
33
|
org = lookup_org!
|
34
|
-
sint =
|
35
|
-
|
36
|
-
sint = lookup_service_integration!(org, identifier) if identifier.present?
|
34
|
+
sint = lookup_service_integration!(org, params[:service_integration_identifier])
|
35
|
+
url = params[:url]
|
37
36
|
webhook_sub = Webhookdb::WebhookSubscription.create(
|
38
37
|
webhook_secret: params[:webhook_secret],
|
39
|
-
deliver_to_url:
|
40
|
-
organization: sint ? nil : org,
|
38
|
+
deliver_to_url: url,
|
41
39
|
service_integration: sint,
|
42
40
|
created_by: current_customer,
|
43
41
|
)
|
44
|
-
message =
|
45
|
-
"All webhooks for this #{sint.service_name} integration will be sent to #{params[:url]}"
|
46
|
-
else
|
47
|
-
"All webhooks for all integrations belonging to organization #{org.name} will be sent to #{params[:url]}."
|
48
|
-
end
|
42
|
+
message = "All notifications for this #{sint.service_name} integration will be sent to #{url}"
|
49
43
|
status 200
|
50
44
|
present webhook_sub, with: Webhookdb::API::WebhookSubscriptionEntity, message:
|
51
45
|
end
|
data/lib/webhookdb/api.rb
CHANGED
@@ -60,13 +60,22 @@ module Webhookdb::API
|
|
60
60
|
return org
|
61
61
|
end
|
62
62
|
|
63
|
-
|
63
|
+
# rubocop:disable Naming/PredicateName
|
64
|
+
def has_admin?(org=nil, customer: nil)
|
65
|
+
# rubocop:enable Naming/PredicateName
|
64
66
|
customer ||= current_customer
|
65
67
|
org ||= lookup_org!
|
66
68
|
has_no_admin = org.verified_memberships_dataset.
|
67
69
|
where(customer:, membership_role: Webhookdb::Role.admin_role).
|
68
70
|
empty?
|
69
|
-
|
71
|
+
return !has_no_admin
|
72
|
+
end
|
73
|
+
|
74
|
+
def ensure_admin!(org=nil, customer: nil)
|
75
|
+
org ||= lookup_org!
|
76
|
+
admin = has_admin?(org, customer:)
|
77
|
+
# noinspection RubyNilAnalysis
|
78
|
+
permission_error!("You don't have admin privileges with #{org.name}.") unless admin
|
70
79
|
end
|
71
80
|
end
|
72
81
|
|
data/lib/webhookdb/apps.rb
CHANGED
@@ -17,6 +17,7 @@ require "webhookdb/api/install"
|
|
17
17
|
require "webhookdb/api/me"
|
18
18
|
require "webhookdb/api/organizations"
|
19
19
|
require "webhookdb/api/replay"
|
20
|
+
require "webhookdb/api/saved_queries"
|
20
21
|
require "webhookdb/api/service_integrations"
|
21
22
|
require "webhookdb/api/services"
|
22
23
|
require "webhookdb/api/stripe"
|
@@ -68,6 +69,7 @@ module Webhookdb::Apps
|
|
68
69
|
mount Webhookdb::API::Me
|
69
70
|
mount Webhookdb::API::Organizations
|
70
71
|
mount Webhookdb::API::Replay
|
72
|
+
mount Webhookdb::API::SavedQueries
|
71
73
|
mount Webhookdb::API::ServiceIntegrations
|
72
74
|
mount Webhookdb::API::Services
|
73
75
|
mount Webhookdb::API::Stripe
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Webhookdb
|
4
|
+
module Envfixer
|
5
|
+
# If DOCKER_DEV is set, replace 'localhost' urls with 'host.docker.internal'.
|
6
|
+
def self.replace_localhost_for_docker(env)
|
7
|
+
return unless env["DOCKER_DEV"]
|
8
|
+
env.each do |k, v|
|
9
|
+
begin
|
10
|
+
localhost = URI(v).host == "localhost"
|
11
|
+
rescue StandardError
|
12
|
+
next
|
13
|
+
end
|
14
|
+
next unless localhost
|
15
|
+
env[k] = v.gsub("localhost", "host.docker.internal")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# If MERGE_HEROKU_ENV, merge all of its environment vars into the current env
|
20
|
+
def self.merge_heroku_env(env)
|
21
|
+
return unless (heroku_app = env.fetch("MERGE_HEROKU_ENV", nil))
|
22
|
+
text = `heroku config -j --app=#{heroku_app}`
|
23
|
+
json = Oj.load(text)
|
24
|
+
json.each do |k, v|
|
25
|
+
env[k] = v
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faker"
|
4
|
+
|
5
|
+
require "webhookdb"
|
6
|
+
require "webhookdb/fixtures"
|
7
|
+
|
8
|
+
module Webhookdb::Fixtures::SavedQueries
|
9
|
+
extend Webhookdb::Fixtures
|
10
|
+
|
11
|
+
fixtured_class Webhookdb::SavedQuery
|
12
|
+
|
13
|
+
base :saved_query do
|
14
|
+
self.description ||= Faker::Lorem.sentence
|
15
|
+
self.sql ||= "SELECT * FROM mytable"
|
16
|
+
end
|
17
|
+
|
18
|
+
before_saving do |instance|
|
19
|
+
instance.organization ||= Webhookdb::Fixtures.organization.create
|
20
|
+
instance
|
21
|
+
end
|
22
|
+
|
23
|
+
decorator :created_by do |c={}|
|
24
|
+
c = Webhookdb::Fixtures.customer.create(c) unless c.is_a?(Webhookdb::Customer)
|
25
|
+
self.created_by = c
|
26
|
+
end
|
27
|
+
end
|
@@ -9,6 +9,12 @@ module Webhookdb::GoogleCalendar
|
|
9
9
|
# How many calendars/events should we fetch in a single page?
|
10
10
|
# Higher uses slightly more memory but fewer API calls.
|
11
11
|
# Max of 2500.
|
12
|
+
#
|
13
|
+
# **NOTE:** Changing this in production will
|
14
|
+
# INVALIDATE EXISTING RESOURCES CAUSING A FULL RESYNC.
|
15
|
+
# You should, in general, avoid modifying this number once there is
|
16
|
+
# much Google Calendar data stored. Instead, page sizes will be automatically reduced
|
17
|
+
# as requests time out.
|
12
18
|
setting :list_page_size, 2000
|
13
19
|
# How many rows should we upsert at a time?
|
14
20
|
# Higher is fewer upserts, but can create very large SQL strings,
|
data/lib/webhookdb/icalendar.rb
CHANGED
@@ -13,5 +13,11 @@ module Webhookdb::Icalendar
|
|
13
13
|
# Do not store events older then this when syncing recurring events.
|
14
14
|
# Many icalendar feeds are misconfigured and this prevents enumerating 2000+ years of recurrence.
|
15
15
|
setting :oldest_recurring_event, "1990-01-01", convert: ->(s) { Date.parse(s) }
|
16
|
+
# Sync icalendar calendars only this often.
|
17
|
+
# Most services only update every day or so. Assume it takes 5s to sync each feed (request, parse, upsert).
|
18
|
+
# If you have 10,000 feeds, that is 50,000 seconds, or almost 14 hours of processing time,
|
19
|
+
# or two threads for 7 hours. The resyncs are spread out across the sync period
|
20
|
+
# (ie, no thundering herd every 8 hours), but it is still a good idea to sync as infrequently as possible.
|
21
|
+
setting :sync_period_hours, 6
|
16
22
|
end
|
17
23
|
end
|
@@ -3,6 +3,10 @@
|
|
3
3
|
require "webhookdb/async/scheduled_job"
|
4
4
|
require "webhookdb/jobs"
|
5
5
|
|
6
|
+
# For every IcalendarCalendar row needing a sync (across all service integrations),
|
7
|
+
# enqueue a +Webhookdb::Jobs::IcalendarSync+ job.
|
8
|
+
# Jobs are 'splayed' over 1/4 of the configured calendar sync period (see +Webhookdb::Icalendar+)
|
9
|
+
# to avoid a thundering herd.
|
6
10
|
class Webhookdb::Jobs::IcalendarEnqueueSyncs
|
7
11
|
extend Webhookdb::Async::ScheduledJob
|
8
12
|
|
@@ -10,12 +14,14 @@ class Webhookdb::Jobs::IcalendarEnqueueSyncs
|
|
10
14
|
splay 30
|
11
15
|
|
12
16
|
def _perform
|
17
|
+
max_splay = Webhookdb::Icalendar.sync_period_hours.hours.to_i / 4
|
13
18
|
Webhookdb::ServiceIntegration.dataset.where_each(service_name: "icalendar_calendar_v1") do |sint|
|
14
19
|
sint.replicator.admin_dataset do |ds|
|
15
20
|
sint.replicator.rows_needing_sync(ds).each do |row|
|
16
21
|
calendar_external_id = row.fetch(:external_id)
|
17
22
|
self.with_log_tags(sint.log_tags) do
|
18
|
-
|
23
|
+
splay = rand(1..max_splay)
|
24
|
+
enqueued_job_id = Webhookdb::Jobs::IcalendarSync.perform_in(splay, sint.id, calendar_external_id)
|
19
25
|
self.logger.info("enqueued_icalendar_sync", calendar_external_id:, enqueued_job_id:)
|
20
26
|
end
|
21
27
|
end
|
@@ -15,8 +15,8 @@ class Webhookdb::Jobs::PrepareDatabaseConnections
|
|
15
15
|
org.db.transaction do
|
16
16
|
# If creating the public host fails, we end up with an orphaned database,
|
17
17
|
# but that's not a big deal- we can eventually see it's empty/unlinked and drop it.
|
18
|
-
org.prepare_database_connections
|
19
|
-
org.create_public_host_cname
|
18
|
+
org.prepare_database_connections(safe: true)
|
19
|
+
org.create_public_host_cname(safe: true)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -9,7 +9,7 @@ module Webhookdb::Jobs
|
|
9
9
|
# Create a single way to do the common task of automatic scheduled backfills.
|
10
10
|
# Each integration that needs automated backfills can add a specification here.
|
11
11
|
module ScheduledBackfills
|
12
|
-
Spec = Struct.new(:klass, :service_name, :cron_expr, :splay, :incremental)
|
12
|
+
Spec = Struct.new(:klass, :service_name, :cron_expr, :splay, :incremental, :recursive)
|
13
13
|
|
14
14
|
# @param spec [Spec]
|
15
15
|
def self.install(spec)
|
@@ -21,7 +21,8 @@ module Webhookdb::Jobs
|
|
21
21
|
|
22
22
|
define_method(:_perform) do
|
23
23
|
Webhookdb::ServiceIntegration.dataset.where_each(service_name: spec.service_name) do |sint|
|
24
|
-
|
24
|
+
m = spec.recursive ? :create_recursive : :create
|
25
|
+
Webhookdb::BackfillJob.send(m, service_integration: sint, incremental: spec.incremental).enqueue
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -31,19 +32,19 @@ module Webhookdb::Jobs
|
|
31
32
|
[
|
32
33
|
Spec.new(
|
33
34
|
"ConvertkitBroadcastBackfill", "convertkit_broadcast_v1",
|
34
|
-
"10 * * * *", 2.minutes, false,
|
35
|
+
"10 * * * *", 2.minutes, false, false,
|
35
36
|
),
|
36
37
|
Spec.new(
|
37
38
|
"ConvertkitSubscriberBackfill", "convertkit_subscriber_v1",
|
38
|
-
"20 * * * *", 2.minutes, true,
|
39
|
+
"20 * * * *", 2.minutes, true, false,
|
39
40
|
),
|
40
41
|
Spec.new(
|
41
42
|
"ConvertkitTagBackfill", "convertkit_tag_v1",
|
42
|
-
"30 * * * *", 2.minutes, false,
|
43
|
+
"30 * * * *", 2.minutes, false, false,
|
43
44
|
),
|
44
45
|
Spec.new(
|
45
46
|
"EmailOctopusScheduledBackfill", "email_octopus_list_v1",
|
46
|
-
Webhookdb::EmailOctopus.cron_expression, 2.minutes, false,
|
47
|
+
Webhookdb::EmailOctopus.cron_expression, 2.minutes, false, true,
|
47
48
|
),
|
48
49
|
Spec.new(
|
49
50
|
"GithubRepoActivityScheduledBackfill", "github_repository_event_v1",
|
@@ -51,31 +52,31 @@ module Webhookdb::Jobs
|
|
51
52
|
),
|
52
53
|
Spec.new(
|
53
54
|
"IntercomScheduledBackfill", "intercom_marketplace_root_v1",
|
54
|
-
"*/1 * * * *", 0, false,
|
55
|
+
"*/1 * * * *", 0, false, true,
|
55
56
|
),
|
56
57
|
Spec.new(
|
57
58
|
"AtomSingleFeedPoller", "atom_single_feed_v1",
|
58
|
-
"11 * * * *", 10.seconds, true,
|
59
|
+
"11 * * * *", 10.seconds, true, false,
|
59
60
|
),
|
60
61
|
Spec.new(
|
61
62
|
"SponsyScheduledBackfill", "sponsy_publication_v1",
|
62
|
-
Webhookdb::Sponsy.cron_expression, 30.seconds, true,
|
63
|
+
Webhookdb::Sponsy.cron_expression, 30.seconds, true, true,
|
63
64
|
),
|
64
65
|
Spec.new(
|
65
66
|
"TransistorEpisodeBackfill", "transistor_episode_v1",
|
66
|
-
Webhookdb::Transistor.episode_cron_expression, 2.minutes, true,
|
67
|
+
Webhookdb::Transistor.episode_cron_expression, 2.minutes, true, true,
|
67
68
|
),
|
68
69
|
Spec.new(
|
69
70
|
"TransistorShowBackfill", "transistor_show_v1",
|
70
|
-
Webhookdb::Transistor.show_cron_expression, 2.minutes, true,
|
71
|
+
Webhookdb::Transistor.show_cron_expression, 2.minutes, true, false,
|
71
72
|
),
|
72
73
|
Spec.new(
|
73
74
|
"TwilioSmsBackfill", "twilio_sms_v1",
|
74
|
-
"*/1 * * * *", 0, true,
|
75
|
+
"*/1 * * * *", 0, true, false,
|
75
76
|
),
|
76
77
|
Spec.new(
|
77
78
|
"SignalwireMessageBackfill", "signalwire_message_v1",
|
78
|
-
"*/1 * * * *", 0, true,
|
79
|
+
"*/1 * * * *", 0, true, false,
|
79
80
|
),
|
80
81
|
].each { |sp| self.install(sp) }
|
81
82
|
end
|
@@ -34,6 +34,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
34
34
|
adder: ->(om) { om.update(organization_id: id, verified: false) },
|
35
35
|
order: :id
|
36
36
|
one_to_many :service_integrations, class: "Webhookdb::ServiceIntegration", order: :id
|
37
|
+
one_to_many :saved_queries, class: "Webhookdb::SavedQuery", order: :id
|
37
38
|
one_to_many :webhook_subscriptions, class: "Webhookdb::WebhookSubscription", order: :id
|
38
39
|
many_to_many :feature_roles, class: "Webhookdb::Role", join_table: :feature_roles_organizations, right_key: :role_id
|
39
40
|
one_to_many :all_webhook_subscriptions,
|
@@ -204,6 +205,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
204
205
|
end
|
205
206
|
|
206
207
|
# Build the org-specific users, database, and set our connection URLs to it.
|
208
|
+
# @param safe [*] If true, noop if connection urls are set.
|
207
209
|
def prepare_database_connections(safe: false)
|
208
210
|
self.db.transaction do
|
209
211
|
self.lock!
|
@@ -220,13 +222,17 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
220
222
|
end
|
221
223
|
|
222
224
|
# Create a CNAME in Cloudflare for the currently configured connection urls.
|
223
|
-
|
225
|
+
# @param safe [*] If true, noop if the public host is set.
|
226
|
+
def create_public_host_cname(safe: false)
|
224
227
|
self.db.transaction do
|
225
228
|
self.lock!
|
226
229
|
# We must have a host to create a CNAME to.
|
227
230
|
raise Webhookdb::InvalidPrecondition, "connection urls must be set" if self.readonly_connection_url_raw.blank?
|
228
231
|
# Should only be used once when creating the org DBs.
|
229
|
-
|
232
|
+
if self.public_host.present?
|
233
|
+
return if safe
|
234
|
+
raise Webhookdb::InvalidPrecondition, "public_host must not be set"
|
235
|
+
end
|
230
236
|
# Use the raw URL, even though we know at this point
|
231
237
|
# public_host is empty so raw and public host urls are the same.
|
232
238
|
Webhookdb::Organization::DbBuilder.new(self).create_public_host_cname(self.readonly_connection_url_raw)
|
data/lib/webhookdb/postgres.rb
CHANGED
@@ -60,6 +60,7 @@ module Webhookdb::Postgres
|
|
60
60
|
"webhookdb/organization/database_migration",
|
61
61
|
"webhookdb/organization_membership",
|
62
62
|
"webhookdb/role",
|
63
|
+
"webhookdb/saved_query",
|
63
64
|
"webhookdb/service_integration",
|
64
65
|
"webhookdb/subscription",
|
65
66
|
"webhookdb/sync_target",
|
@@ -120,11 +121,13 @@ module Webhookdb::Postgres
|
|
120
121
|
end
|
121
122
|
|
122
123
|
def self.run_all_migrations(target: nil)
|
124
|
+
# :nocov:
|
123
125
|
Sequel.extension :migration
|
124
126
|
Webhookdb::Postgres.each_model_superclass do |cls|
|
125
127
|
cls.install_all_extensions
|
126
128
|
Sequel::Migrator.run(cls.db, Pathname(__FILE__).dirname.parent.parent + "db/migrations", target:)
|
127
129
|
end
|
130
|
+
# :nocov:
|
128
131
|
end
|
129
132
|
|
130
133
|
# We can always register the models right away, since it does not have a side effect.
|
@@ -263,7 +263,7 @@ All of this information can be found in the WebhookDB docs, at https://docs.webh
|
|
263
263
|
item[:front_message_id] = front_response_body.fetch("message_uid")
|
264
264
|
end
|
265
265
|
else
|
266
|
-
messaged_at = Time.at(item.fetch(:data).fetch("created_at"))
|
266
|
+
messaged_at = Time.at(item.fetch(:data).fetch("payload").fetch("created_at"))
|
267
267
|
if messaged_at < Webhookdb::Front.channel_sync_refreshness_cutoff.seconds.ago
|
268
268
|
# Do not sync old rows, just mark them synced
|
269
269
|
item[:signalwire_sid] = "skipped_due_to_age"
|
@@ -125,10 +125,9 @@ The secret to use for signing is:
|
|
125
125
|
end
|
126
126
|
|
127
127
|
CLEANUP_SERVICE_NAMES = ["icalendar_event_v1"].freeze
|
128
|
-
SYNC_PERIOD = 4.hours
|
129
128
|
|
130
129
|
def rows_needing_sync(dataset, now: Time.now)
|
131
|
-
cutoff = now -
|
130
|
+
cutoff = now - Webhookdb::Icalendar.sync_period_hours.hours
|
132
131
|
return dataset.where(Sequel[last_synced_at: nil] | Sequel.expr { last_synced_at < cutoff })
|
133
132
|
end
|
134
133
|
|
@@ -215,7 +214,11 @@ The secret to use for signing is:
|
|
215
214
|
expected_errors = [
|
216
215
|
417, # If someone uses an Outlook HTML calendar, fetch gives us a 417
|
217
216
|
]
|
218
|
-
|
217
|
+
# For most client errors, we can't do anything about it. For example,
|
218
|
+
# and 'unshared' URL could result in a 401, 403, 404, or even a 405.
|
219
|
+
# For now, other client errors, we can raise on,
|
220
|
+
# in case it's something we can fix/work around.
|
221
|
+
is_problem_error = (response_status > 405 || response_status < 400) &&
|
219
222
|
!expected_errors.include?(response_status)
|
220
223
|
raise e if is_problem_error
|
221
224
|
response_body = e.response.body.to_s
|
@@ -155,7 +155,9 @@ Press 'Show' next to the newly-created API token, and copy it.)
|
|
155
155
|
def _fetch_backfill_page(pagination_token, last_backfilled:)
|
156
156
|
urltail = pagination_token
|
157
157
|
if pagination_token.blank?
|
158
|
-
|
158
|
+
# We need to handle positive and negative UTC offset running locally (non-UTC).
|
159
|
+
# Using UTC + 1 day would give 'today' in some cases, we always want 'tomorrow the day after'.
|
160
|
+
date_send_max = (Time.now.utc + 2.days).to_date
|
159
161
|
urltail = "/2010-04-01/Accounts/#{self.service_integration.backfill_key}/Messages.json" \
|
160
162
|
"?PageSize=100&DateSend%3C=#{date_send_max}"
|
161
163
|
end
|
@@ -128,7 +128,9 @@ Both of these values should be visible from the homepage of your Twilio admin Da
|
|
128
128
|
def _fetch_backfill_page(pagination_token, last_backfilled:)
|
129
129
|
url = "https://api.twilio.com"
|
130
130
|
if pagination_token.blank?
|
131
|
-
|
131
|
+
# We need to handle positive and negative UTC offset running locally (non-UTC).
|
132
|
+
# Using UTC + 1 day would give 'today' in some cases, we always want 'tomorrow the day after'.
|
133
|
+
date_send_max = (Time.now.utc + 2.days).to_date
|
132
134
|
url += "/2010-04-01/Accounts/#{self.service_integration.backfill_key}/Messages.json" \
|
133
135
|
"?PageSize=100&DateSend%3C=#{date_send_max}"
|
134
136
|
else
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Webhookdb::SavedQuery < Webhookdb::Postgres::Model(:saved_queries)
|
4
|
+
plugin :timestamps
|
5
|
+
|
6
|
+
CLI_EDITABLE_FIELDS = ["description", "sql", "public"].freeze
|
7
|
+
INFO_FIELDS = {
|
8
|
+
"id" => :opaque_id,
|
9
|
+
"description" => :description,
|
10
|
+
"public" => :public,
|
11
|
+
"run_url" => :run_url,
|
12
|
+
"sql" => :sql,
|
13
|
+
}.freeze
|
14
|
+
DOCS_URL = "https://docs.webhookdb.com/docs/integrating/saved-queries.html"
|
15
|
+
|
16
|
+
many_to_one :organization, class: "Webhookdb::Organization"
|
17
|
+
many_to_one :created_by, class: "Webhookdb::Customer"
|
18
|
+
|
19
|
+
alias public? public
|
20
|
+
def private? = !self.public?
|
21
|
+
|
22
|
+
def before_create
|
23
|
+
self[:opaque_id] ||= Webhookdb::Id.new_opaque_id("svq")
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_url = "#{Webhookdb.api_url}/v1/saved_queries/#{self.opaque_id}/run"
|
28
|
+
end
|
data/lib/webhookdb/version.rb
CHANGED
data/lib/webhookdb.rb
CHANGED
@@ -8,29 +8,12 @@ require "money"
|
|
8
8
|
require "pathname"
|
9
9
|
require "phony"
|
10
10
|
|
11
|
+
require "webhookdb/envfixer"
|
11
12
|
require "webhookdb/json"
|
13
|
+
require "webhookdb/method_utilities"
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
ENV.each do |k, v|
|
16
|
-
begin
|
17
|
-
localhost = URI(v).host == "localhost"
|
18
|
-
rescue StandardError
|
19
|
-
next
|
20
|
-
end
|
21
|
-
next unless localhost
|
22
|
-
ENV[k] = v.gsub("localhost", "host.docker.internal")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
if (heroku_app = ENV.fetch("MERGE_HEROKU_ENV", nil))
|
27
|
-
# If MERGE_HEROKU_ENV, merge all of its environment vars into the current env
|
28
|
-
text = `heroku config -j --app=#{heroku_app}`
|
29
|
-
json = Oj.load(text)
|
30
|
-
json.each do |k, v|
|
31
|
-
ENV[k] = v
|
32
|
-
end
|
33
|
-
end
|
15
|
+
Webhookdb::Envfixer.replace_localhost_for_docker(ENV)
|
16
|
+
Webhookdb::Envfixer.merge_heroku_env(ENV)
|
34
17
|
|
35
18
|
Money.locale_backend = :i18n
|
36
19
|
Money.default_currency = "USD"
|
@@ -50,6 +33,7 @@ end
|
|
50
33
|
module Webhookdb
|
51
34
|
include Appydays::Loggable
|
52
35
|
include Appydays::Configurable
|
36
|
+
extend Webhookdb::MethodUtilities
|
53
37
|
|
54
38
|
# Error raised when we cannot take an action
|
55
39
|
# because some condition has not been set up right.
|
@@ -86,6 +70,9 @@ module Webhookdb
|
|
86
70
|
|
87
71
|
DATA_DIR = Pathname(__FILE__).dirname.parent + "data"
|
88
72
|
|
73
|
+
singleton_attr_reader :globals_cache
|
74
|
+
@globals_cache = {}
|
75
|
+
|
89
76
|
configurable(:webhookdb) do
|
90
77
|
setting :log_level_override,
|
91
78
|
nil,
|
@@ -100,6 +87,10 @@ module Webhookdb
|
|
100
87
|
setting :support_email, "hello@webhookdb.com"
|
101
88
|
setting :use_globals_cache, false
|
102
89
|
setting :regression_mode, false
|
90
|
+
|
91
|
+
after_configured do
|
92
|
+
globals_cache.clear
|
93
|
+
end
|
103
94
|
end
|
104
95
|
|
105
96
|
# Regression mode is true when we re replaying webhooks locally,
|
@@ -110,9 +101,6 @@ module Webhookdb
|
|
110
101
|
return self.regression_mode
|
111
102
|
end
|
112
103
|
|
113
|
-
require "webhookdb/method_utilities"
|
114
|
-
extend Webhookdb::MethodUtilities
|
115
|
-
|
116
104
|
require "webhookdb/sentry"
|
117
105
|
|
118
106
|
def self.load_app
|
@@ -129,9 +117,6 @@ module Webhookdb
|
|
129
117
|
# :section: Globals cache
|
130
118
|
#
|
131
119
|
|
132
|
-
singleton_attr_reader :globals_cache
|
133
|
-
@globals_cache = {}
|
134
|
-
|
135
120
|
# If globals caching is enabled, see if there is a cached value under +key+
|
136
121
|
# and return it if so. If there is not, evaluate the given block and store that value.
|
137
122
|
# Generally used for looking up well-known database objects like certain roles.
|
@@ -141,7 +126,7 @@ module Webhookdb
|
|
141
126
|
return result if result
|
142
127
|
end
|
143
128
|
result = yield()
|
144
|
-
self.globals_cache[key] = result
|
129
|
+
(self.globals_cache[key] = result) if self.use_globals_cache
|
145
130
|
return result
|
146
131
|
end
|
147
132
|
|
@@ -167,18 +152,6 @@ module Webhookdb
|
|
167
152
|
return key
|
168
153
|
end
|
169
154
|
|
170
|
-
#
|
171
|
-
# :section: Unambiguous/promo code chars
|
172
|
-
#
|
173
|
-
|
174
|
-
# Remove ambiguous characters (L, I, 1 or 0, O) and vowels from possible codes
|
175
|
-
# to avoid creating ambiguous codes or real words.
|
176
|
-
UNAMBIGUOUS_CHARS = "CDFGHJKMNPQRTVWXYZ23469".chars.freeze
|
177
|
-
|
178
|
-
def self.take_unambiguous_chars(n)
|
179
|
-
return Array.new(n) { UNAMBIGUOUS_CHARS.sample }.join
|
180
|
-
end
|
181
|
-
|
182
155
|
# Convert a string into something we consistently use for slugs:
|
183
156
|
# a-z, 0-9, and underscores only. Leading numbers are converted to words.
|
184
157
|
#
|
@@ -206,6 +179,17 @@ module Webhookdb
|
|
206
179
|
"9" => "nine",
|
207
180
|
}.freeze
|
208
181
|
|
182
|
+
def self.parse_bool(s)
|
183
|
+
# rubocop:disable Style/NumericPredicate
|
184
|
+
return false if s == nil? || s.blank? || s == 0
|
185
|
+
# rubocop:enable Style/NumericPredicate
|
186
|
+
return true if s.is_a?(Integer)
|
187
|
+
sb = s.to_s.downcase
|
188
|
+
return true if ["true", "t", "yes", "y", "on", "1"].include?(sb)
|
189
|
+
return false if ["false", "f", "no", "n", "off", "0"].include?(sb)
|
190
|
+
raise ArgumentError, "unparseable bool: #{s.inspect}"
|
191
|
+
end
|
192
|
+
|
209
193
|
# Return the request user and admin stored in TLS. See service.rb for implementation.
|
210
194
|
#
|
211
195
|
# Note that the second return value (the admin) will be nil if not authed as an admin,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: webhookdb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- WebhookDB
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -853,6 +853,7 @@ files:
|
|
853
853
|
- db/migrations/036_oauth.rb
|
854
854
|
- db/migrations/037_oauth_used.rb
|
855
855
|
- db/migrations/038_webhookdb_api_key.rb
|
856
|
+
- db/migrations/039_saved_query.rb
|
856
857
|
- integration/async_spec.rb
|
857
858
|
- integration/auth_spec.rb
|
858
859
|
- integration/database_spec.rb
|
@@ -883,6 +884,7 @@ files:
|
|
883
884
|
- lib/webhookdb/api/me.rb
|
884
885
|
- lib/webhookdb/api/organizations.rb
|
885
886
|
- lib/webhookdb/api/replay.rb
|
887
|
+
- lib/webhookdb/api/saved_queries.rb
|
886
888
|
- lib/webhookdb/api/service_integrations.rb
|
887
889
|
- lib/webhookdb/api/services.rb
|
888
890
|
- lib/webhookdb/api/stripe.rb
|
@@ -918,6 +920,7 @@ files:
|
|
918
920
|
- lib/webhookdb/developer_alert.rb
|
919
921
|
- lib/webhookdb/email_octopus.rb
|
920
922
|
- lib/webhookdb/enumerable.rb
|
923
|
+
- lib/webhookdb/envfixer.rb
|
921
924
|
- lib/webhookdb/fixtures.rb
|
922
925
|
- lib/webhookdb/fixtures/backfill_jobs.rb
|
923
926
|
- lib/webhookdb/fixtures/customers.rb
|
@@ -930,6 +933,7 @@ files:
|
|
930
933
|
- lib/webhookdb/fixtures/organization_memberships.rb
|
931
934
|
- lib/webhookdb/fixtures/organizations.rb
|
932
935
|
- lib/webhookdb/fixtures/reset_codes.rb
|
936
|
+
- lib/webhookdb/fixtures/saved_queries.rb
|
933
937
|
- lib/webhookdb/fixtures/service_integrations.rb
|
934
938
|
- lib/webhookdb/fixtures/subscriptions.rb
|
935
939
|
- lib/webhookdb/fixtures/sync_targets.rb
|
@@ -1103,6 +1107,7 @@ files:
|
|
1103
1107
|
- lib/webhookdb/replicator/webhook_request.rb
|
1104
1108
|
- lib/webhookdb/replicator/webhookdb_customer_v1.rb
|
1105
1109
|
- lib/webhookdb/role.rb
|
1110
|
+
- lib/webhookdb/saved_query.rb
|
1106
1111
|
- lib/webhookdb/sentry.rb
|
1107
1112
|
- lib/webhookdb/service.rb
|
1108
1113
|
- lib/webhookdb/service/auth.rb
|