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.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/admin-dist/assets/index-6aebf805.js +264 -0
  3. data/admin-dist/favicon.ico +0 -0
  4. data/admin-dist/index.html +130 -0
  5. data/admin-dist/manifest.json +15 -0
  6. data/data/messages/replicators/url-recorder.liquid +20 -0
  7. data/data/messages/templates/errors/signalwire_send_sms.email.liquid +31 -0
  8. data/data/messages/web/install-customer-login.liquid +6 -5
  9. data/data/messages/web/install-error.liquid +1 -1
  10. data/data/messages/web/install-forbidden.liquid +25 -0
  11. data/data/messages/web/install-org-chooser.liquid +40 -0
  12. data/data/messages/web/install-success.liquid +2 -1
  13. data/data/messages/web/install.liquid +2 -1
  14. data/data/messages/web/partials/head.liquid +2 -0
  15. data/data/messages/web/styles.liquid +24 -0
  16. data/db/migrations/041_views.rb +20 -0
  17. data/db/migrations/042_sint_lock.rb +10 -0
  18. data/db/migrations/043_text_search.rb +28 -0
  19. data/db/migrations/044_oauth_session_token_cache.rb +21 -0
  20. data/integration/auth_spec.rb +2 -2
  21. data/lib/sequel/plugins/text_searchable.rb +165 -0
  22. data/lib/sequel/text_searchable.rb +42 -0
  23. data/lib/webhookdb/admin_api/auth.rb +24 -3
  24. data/lib/webhookdb/admin_api/data_provider.rb +196 -0
  25. data/lib/webhookdb/admin_api/entities.rb +143 -28
  26. data/lib/webhookdb/admin_api.rb +0 -2
  27. data/lib/webhookdb/api/auth.rb +5 -6
  28. data/lib/webhookdb/api/db.rb +31 -6
  29. data/lib/webhookdb/api/entities.rb +7 -1
  30. data/lib/webhookdb/api/helpers.rb +6 -25
  31. data/lib/webhookdb/api/install.rb +204 -79
  32. data/lib/webhookdb/api/organizations.rb +14 -12
  33. data/lib/webhookdb/api/saved_queries.rb +9 -3
  34. data/lib/webhookdb/api/saved_views.rb +99 -0
  35. data/lib/webhookdb/api/service_integrations.rb +15 -9
  36. data/lib/webhookdb/api/subscriptions.rb +3 -1
  37. data/lib/webhookdb/api/sync_targets.rb +9 -7
  38. data/lib/webhookdb/api/system.rb +1 -0
  39. data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
  40. data/lib/webhookdb/apps.rb +30 -7
  41. data/lib/webhookdb/async/audit_logger.rb +2 -0
  42. data/lib/webhookdb/async.rb +5 -0
  43. data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
  44. data/lib/webhookdb/backfill_job.rb +9 -0
  45. data/lib/webhookdb/customer.rb +5 -0
  46. data/lib/webhookdb/database_document.rb +1 -1
  47. data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
  48. data/lib/webhookdb/db_adapter.rb +20 -4
  49. data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
  50. data/lib/webhookdb/fixtures/organizations.rb +5 -0
  51. data/lib/webhookdb/fixtures/roles.rb +14 -0
  52. data/lib/webhookdb/fixtures/saved_views.rb +25 -0
  53. data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
  54. data/lib/webhookdb/http.rb +8 -2
  55. data/lib/webhookdb/icalendar.rb +3 -0
  56. data/lib/webhookdb/idempotency.rb +69 -22
  57. data/lib/webhookdb/increase.rb +69 -21
  58. data/lib/webhookdb/intercom.rb +10 -3
  59. data/lib/webhookdb/jobs/backfill.rb +3 -1
  60. data/lib/webhookdb/jobs/emailer.rb +0 -1
  61. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
  62. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
  63. data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
  64. data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
  65. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
  66. data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
  67. data/lib/webhookdb/message/body.rb +6 -4
  68. data/lib/webhookdb/message/delivery.rb +2 -0
  69. data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
  70. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
  71. data/lib/webhookdb/oauth/fake_provider.rb +44 -0
  72. data/lib/webhookdb/oauth/front_provider.rb +1 -2
  73. data/lib/webhookdb/oauth/increase_provider.rb +80 -0
  74. data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
  75. data/lib/webhookdb/oauth/session.rb +20 -0
  76. data/lib/webhookdb/oauth.rb +7 -21
  77. data/lib/webhookdb/organization/alerting.rb +2 -0
  78. data/lib/webhookdb/organization/database_migration.rb +3 -0
  79. data/lib/webhookdb/organization.rb +37 -6
  80. data/lib/webhookdb/organization_membership.rb +14 -7
  81. data/lib/webhookdb/postgres.rb +2 -0
  82. data/lib/webhookdb/replicator/base.rb +1 -0
  83. data/lib/webhookdb/replicator/docgen.rb +9 -1
  84. data/lib/webhookdb/replicator/fake.rb +2 -3
  85. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
  86. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
  87. data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
  88. data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
  89. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
  90. data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
  91. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
  92. data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
  93. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
  94. data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
  95. data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
  96. data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
  97. data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
  98. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
  99. data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
  100. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
  101. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
  102. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
  103. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
  104. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  105. data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
  106. data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
  107. data/lib/webhookdb/replicator/webhook_request.rb +4 -0
  108. data/lib/webhookdb/replicator.rb +8 -0
  109. data/lib/webhookdb/role.rb +5 -2
  110. data/lib/webhookdb/saved_query.rb +23 -0
  111. data/lib/webhookdb/saved_view.rb +73 -0
  112. data/lib/webhookdb/sentry.rb +2 -0
  113. data/lib/webhookdb/service/entities.rb +0 -4
  114. data/lib/webhookdb/service/helpers.rb +5 -0
  115. data/lib/webhookdb/service/middleware.rb +9 -0
  116. data/lib/webhookdb/service/types.rb +10 -8
  117. data/lib/webhookdb/service/validators.rb +1 -2
  118. data/lib/webhookdb/service/view_api.rb +1 -1
  119. data/lib/webhookdb/service_integration.rb +17 -15
  120. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
  121. data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
  122. data/lib/webhookdb/subscription.rb +2 -0
  123. data/lib/webhookdb/sync_target.rb +10 -2
  124. data/lib/webhookdb/tasks/message.rb +3 -1
  125. data/lib/webhookdb/version.rb +1 -1
  126. data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
  127. data/lib/webhookdb/webhook_subscription.rb +2 -0
  128. metadata +57 -9
  129. data/lib/webhookdb/admin_api/customers.rb +0 -63
  130. data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
  131. data/lib/webhookdb/admin_api/roles.rb +0 -15
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module SequelTextSearchable
6
+ VERSION = "0.0.1"
7
+
8
+ INDEX_MODES = [:async, :sync, :off].freeze
9
+ DEFAULT_MODE = :async
10
+
11
+ class << self
12
+ def index_mode = @index_mode || DEFAULT_MODE
13
+
14
+ def index_mode=(v)
15
+ raise ArgumentError, "mode #{v.inspect} must be one of: #{INDEX_MODES}" unless
16
+ INDEX_MODES.include?(v)
17
+ @index_mode = v
18
+ end
19
+
20
+ def searchable_models = @searchable_models ||= []
21
+
22
+ # Return the global threadpool for :async indexing.
23
+ # Use at most a couple threads; if the work gets backed up,
24
+ # have the caller run it. If the threads die,
25
+ # the text update is lost, so we don't want to let it queue up forever.
26
+ def threadpool
27
+ return @threadpool ||= Concurrent::ThreadPoolExecutor.new(
28
+ min_threads: 1,
29
+ max_threads: 2,
30
+ max_queue: 10,
31
+ fallback_policy: :caller_runs,
32
+ )
33
+ end
34
+
35
+ # Set your own threadpool.
36
+ attr_writer :threadpool
37
+
38
+ def reindex_all
39
+ return self.searchable_models.sum(&:text_search_reindex_all)
40
+ end
41
+ end
42
+ end
@@ -8,7 +8,28 @@ class Webhookdb::AdminAPI::Auth < Webhookdb::AdminAPI::V1
8
8
  resource :auth do
9
9
  desc "Return the current administrator customer."
10
10
  get do
11
- present admin_customer, with: Webhookdb::AdminAPI::CurrentCustomerEntity, env:
11
+ present admin_customer, with: Webhookdb::AdminAPI::Entities::CurrentCustomer, env:
12
+ end
13
+
14
+ params do
15
+ requires :email, type: String
16
+ requires :password, type: String
17
+ end
18
+
19
+ resource :login do
20
+ auth(:skip)
21
+ params do
22
+ requires :email, type: String
23
+ requires :password, type: String
24
+ end
25
+ post do
26
+ (me = Webhookdb::Customer.with_email(params[:email])) or unauthenticated!
27
+ me.authenticate(params[:password]) or unauthenticated!
28
+ unauthenticated! unless me.admin?
29
+ set_customer(me)
30
+ status 200
31
+ present admin_customer, with: Webhookdb::AdminAPI::Entities::CurrentCustomer, env:
32
+ end
12
33
  end
13
34
 
14
35
  resource :impersonate do
@@ -17,7 +38,7 @@ class Webhookdb::AdminAPI::Auth < Webhookdb::AdminAPI::V1
17
38
  Webhookdb::Service::Auth::Impersonation.new(env["warden"]).off(admin_customer)
18
39
 
19
40
  status 200
20
- present admin_customer, with: Webhookdb::AdminAPI::CurrentCustomerEntity, env:
41
+ present admin_customer, with: Webhookdb::AdminAPI::Entities::CurrentCustomer, env:
21
42
  end
22
43
 
23
44
  route_param :customer_id, type: Integer do
@@ -28,7 +49,7 @@ class Webhookdb::AdminAPI::Auth < Webhookdb::AdminAPI::V1
28
49
  Webhookdb::Service::Auth::Impersonation.new(env["warden"]).on(target)
29
50
 
30
51
  status 200
31
- present target, with: Webhookdb::AdminAPI::CurrentCustomerEntity, env:
52
+ present target, with: Webhookdb::AdminAPI::Entities::CurrentCustomer, env:
32
53
  end
33
54
  end
34
55
  end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/admin_api"
4
+
5
+ class Webhookdb::AdminAPI::DataProvider < Webhookdb::AdminAPI::V1
6
+ include Webhookdb::AdminAPI::Entities
7
+
8
+ class CustomerRoleModel < Sequel::Model
9
+ many_to_one :customer, class: "Webhookdb::Customer"
10
+ many_to_one :role, class: "Webhookdb::Role"
11
+ end
12
+ CustomerRoleModel.set_dataset(Webhookdb::Postgres::Model.db[:roles_customers])
13
+
14
+ TYPEINFO = {
15
+ backfill_jobs: [Webhookdb::BackfillJob, BackfillJob],
16
+ customers: [Webhookdb::Customer, Customer],
17
+ customer_reset_codes: [Webhookdb::Customer::ResetCode, CustomerResetCode],
18
+ customer_roles: [CustomerRoleModel, CustomerRole],
19
+ logged_webhooks: [Webhookdb::LoggedWebhook, LoggedWebhook],
20
+ message_bodies: [Webhookdb::Message::Body, MessageBody],
21
+ message_deliveries: [Webhookdb::Message::Delivery, MessageDelivery],
22
+ organization_database_migrations: [Webhookdb::Organization::DatabaseMigration, OrganizationDatabaseMigration],
23
+ organization_memberships: [Webhookdb::OrganizationMembership, OrganizationMembership],
24
+ organizations: [Webhookdb::Organization, Organization],
25
+ replicated_databases: [nil, nil],
26
+ roles: [Webhookdb::Role, Role],
27
+ saved_queries: [Webhookdb::SavedQuery, SavedQuery],
28
+ saved_views: [Webhookdb::SavedView, SavedView],
29
+ service_integrations: [Webhookdb::ServiceIntegration, ServiceIntegration],
30
+ subscriptions: [Webhookdb::Subscription, Subscription],
31
+ sync_targets: [Webhookdb::SyncTarget, SyncTarget],
32
+ webhook_subscriptions: [Webhookdb::WebhookSubscription, WebhookSubscription],
33
+ webhook_subscription_deliveries: [Webhookdb::WebhookSubscription::Delivery, WebhookSubscriptionDelivery],
34
+ }.freeze
35
+
36
+ TYPES_FOR_RESOURCES = TYPEINFO.transform_values { |v| v[0] }.freeze
37
+ ENTITIES_FOR_TYPES = TYPEINFO.values.to_h { |v| [v[0], v[1]] }.freeze
38
+
39
+ resource :data_provider do
40
+ helpers do
41
+ params :data_provider_pagination do
42
+ requires :page, type: Integer
43
+ requires :per_page, type: Integer
44
+ end
45
+
46
+ params :data_provider_sort do
47
+ requires :field, type: Symbol
48
+ requires :order, type: Symbol, values: [:ASC, :DESC]
49
+ end
50
+
51
+ params :base_params do
52
+ requires :resource, type: Symbol, values: TYPES_FOR_RESOURCES.keys
53
+ optional :meta, type: JSON
54
+ end
55
+
56
+ params :base_record_params do
57
+ use :base_params
58
+ requires :id, type: Integer
59
+ end
60
+
61
+ params :list_params do
62
+ use :base_params
63
+ optional :pagination, type: JSON do
64
+ use :data_provider_pagination
65
+ end
66
+ optional :sort, type: JSON do
67
+ use :data_provider_sort
68
+ end
69
+ optional :filter, type: JSON
70
+ end
71
+
72
+ params :many_ids_params do
73
+ use :base_params
74
+ requires :ids, type: [Integer]
75
+ end
76
+
77
+ def lookup_model_type
78
+ rt = TYPES_FOR_RESOURCES.fetch(params.fetch(:resource))
79
+ return rt
80
+ end
81
+
82
+ def lookup_model
83
+ cls = lookup_model_type
84
+ m = cls[id: params.fetch(:id)]
85
+ merror!(403, "No #{params[:resource]} with pk #{params[:id]}", code: "forbidden") if m.nil?
86
+ return m
87
+ end
88
+
89
+ def lookup_entity(cls)
90
+ return ENTITIES_FOR_TYPES.fetch(cls)
91
+ end
92
+
93
+ def present_one(item, item_entity)
94
+ data = item_entity.represent(item)
95
+ e = {data:}
96
+ present e
97
+ end
98
+
99
+ def present_dataset(ds, item_entity)
100
+ data = ds.all.map { |o| item_entity.represent(o) }
101
+ total = ds.respond_to?(:pagination_record_count) ? ds.pagination_record_count : ds.limit(nil).offset(nil).count
102
+ e = {data:, total:}
103
+ present e
104
+ end
105
+
106
+ def apply_list_params(ods)
107
+ ds = ods
108
+ if (filter = params[:filter])
109
+ search_term = filter.delete(:q)
110
+ ds = ds.text_search(search_term) if search_term && ds.respond_to?(:text_search)
111
+ ds = ds.where(filter.to_h.transform_keys(&:to_sym)) if filter.present?
112
+ end
113
+ if (sort = params[:sort])
114
+ sort_col = sort[:field]
115
+ unless sort_col.to_s.include?(".")
116
+ # If the sort column includes a '.', we have to special case it.
117
+ # Right now we don't have any allowed sort cols.
118
+ # It is possible to make an unsupported column sortable in react-admin, causing the backend to error,
119
+ # but it's not easy to clear out your filters/sort settings once this happens.
120
+ # So instead, just avoid causing the error, and just no-op if an invalid column is passed in.
121
+ order = Sequel.send(sort[:order].to_s.downcase, sort_col, nulls: :last)
122
+ ds = ds.order(order)
123
+ end
124
+ else
125
+ ds = ds.order(Sequel.desc(ds.model.primary_key))
126
+ end
127
+ if (pagination = params[:pagination])
128
+ ds = ds.paginate(pagination[:page], pagination[:per_page])
129
+ end
130
+ return ds
131
+ end
132
+ end
133
+
134
+ params do
135
+ use :base_record_params
136
+ end
137
+ post :get_one do
138
+ model = lookup_model
139
+ status 200
140
+ present_one model, lookup_entity(model.class)
141
+ end
142
+
143
+ params do
144
+ use :list_params
145
+ end
146
+ post :get_list do
147
+ model_cls = lookup_model_type
148
+ ds = model_cls.dataset
149
+ ds = apply_list_params(ds)
150
+ status 200
151
+ present_dataset ds, lookup_entity(model_cls)
152
+ end
153
+
154
+ params do
155
+ use :many_ids_params
156
+ end
157
+ post :get_many do
158
+ model_cls = lookup_model_type
159
+ ds = model_cls.dataset
160
+ ds = ds.where(id: params.fetch(:ids))
161
+ status 200
162
+ present_dataset ds, lookup_entity(model_cls)
163
+ end
164
+
165
+ params do
166
+ use :list_params
167
+ requires :target, type: Symbol
168
+ requires :id, type: Integer
169
+ end
170
+ post :get_many_reference do
171
+ if params[:resource] == :replicated_databases
172
+ (org = Webhookdb::Organization[params[:id]]) or forbidden!
173
+ rows = org.admin_connection do |db|
174
+ rows = db[Sequel[:information_schema][:tables]].
175
+ where(table_schema: org.replication_schema).
176
+ select(
177
+ Sequel[:table_name].as(:id),
178
+ :table_name,
179
+ Sequel.expr { pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) }.as(:size_pretty),
180
+ Sequel.expr { pg_relation_size(quote_ident(table_name)) }.as(:size),
181
+ ).all
182
+ rows
183
+ end
184
+ status 200
185
+ present({data: rows, total: rows.length})
186
+ else
187
+ model_cls = lookup_model_type
188
+ ds = model_cls.dataset
189
+ ds = ds.where(params[:target] => params[:id])
190
+ ds = apply_list_params(ds)
191
+ status 200
192
+ present_dataset ds, lookup_entity(model_cls)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -5,62 +5,177 @@ require "grape_entity"
5
5
  require "webhookdb/service/entities"
6
6
  require "webhookdb/admin_api" unless defined? Webhookdb::AdminAPI
7
7
 
8
- module Webhookdb::AdminAPI
9
- CurrentCustomerEntity = Webhookdb::Service::Entities::CurrentCustomer
10
- MoneyEntity = Webhookdb::Service::Entities::Money
11
- TimeRangeEntity = Webhookdb::Service::Entities::TimeRange
8
+ module Webhookdb::AdminAPI::Entities
9
+ CurrentCustomer = Webhookdb::Service::Entities::CurrentCustomer
10
+ Money = Webhookdb::Service::Entities::Money
11
+ TimeRange = Webhookdb::Service::Entities::TimeRange
12
12
 
13
- class BaseEntity < Webhookdb::Service::Entities::Base; end
13
+ class Base < Webhookdb::Service::Entities::Base
14
+ expose :id, if: ->(o) { o.respond_to?(:id) }
15
+ expose :created_at, if: ->(o) { o.respond_to?(:created_at) }
16
+ expose :updated_at, if: ->(o) { o.respond_to?(:updated_at) }
17
+ expose :soft_deleted_at, if: ->(o) { o.respond_to?(:soft_deleted_at) }
18
+ expose :admin_link, if: ->(o, _) { o.respond_to?(:admin_link) }
19
+ end
14
20
 
15
- class RoleEntity < BaseEntity
21
+ class Related < Grape::Entity
16
22
  expose :id
17
- expose :name
18
23
  end
19
24
 
20
- class CustomerEntity < BaseEntity
21
- expose :id
22
- expose :created_at
25
+ class BackfillJob < Base
26
+ expose :service_integration, with: Related
27
+ expose :organization, with: Related, &self.delegate_to(:service_integration, :organization)
28
+ expose :started_at
29
+ expose :finished_at
30
+ expose :opaque_id
31
+ expose :parent_job, with: Related
32
+ expose :created_by, with: Related
33
+ expose :incremental
34
+ end
35
+
36
+ class Customer < Base
23
37
  expose :email
24
38
  expose :name
25
39
  expose :note
26
40
  end
27
41
 
28
- class CustomerResetCodes < BaseEntity
29
- expose :id
30
- expose :created_at
42
+ class CustomerResetCode < Base
31
43
  expose :transport
32
44
  expose :token
33
45
  expose :used
34
46
  expose :expire_at
47
+ expose :customer, with: Related
35
48
  end
36
49
 
37
- class DetailedCustomerEntity < CustomerEntity
38
- expose :roles do |instance|
39
- instance.roles.map(&:name)
50
+ class CustomerRole < Base
51
+ expose :customer do
52
+ expose :id, &self.delegate_to(:customer_id)
53
+ expose :email, &self.delegate_to(:customer, :email)
54
+ end
55
+ expose :role do
56
+ expose :id, &self.delegate_to(:role_id)
57
+ expose :name, &self.delegate_to(:role, :name)
40
58
  end
41
- expose :reset_codes, with: CustomerResetCodes
42
59
  end
43
60
 
44
- class MessageBodyEntity < BaseEntity
45
- expose :id
61
+ class LoggedWebhook < Base
62
+ expose :inserted_at
63
+ expose :truncated_at
64
+ expose :request_body
65
+ expose(:request_headers) { |inst| inst.request_headers.to_a }
66
+ expose :response_status
67
+ expose :service_integration_opaque_id
68
+ expose :service_integration, with: Related
69
+ expose :organization, with: Related
70
+ expose :request_method
71
+ expose :request_path
72
+ end
73
+
74
+ class MessageBody < Base
46
75
  expose :content
47
76
  expose :mediatype
77
+ expose :delivery, with: Related
48
78
  end
49
79
 
50
- class MessageDeliveryEntity < BaseEntity
51
- expose :id
52
- expose :created_at
53
- expose :updated_at
54
- expose :soft_deleted_at
80
+ class MessageDelivery < Base
81
+ expose :extra_fields
82
+ expose :recipient, with: Related
83
+ expose :sent_at
55
84
  expose :template
85
+ expose :to
56
86
  expose :transport_type
57
87
  expose :transport_service
58
88
  expose :transport_message_id
59
- expose :sent_at
60
- expose :to
61
89
  end
62
90
 
63
- class MessageDeliveryWithBodiesEntity < MessageDeliveryEntity
64
- expose :bodies, with: MessageBodyEntity
91
+ class Organization < Base
92
+ expose :name
93
+ expose :key
94
+ end
95
+
96
+ class OrganizationDatabaseMigration < Base
97
+ expose :organization, with: Related
98
+ expose :started_at
99
+ expose :finished_at
100
+ expose :started_by, with: Related
101
+ expose :organization_schema
102
+ expose :last_migrated_service_integration_id
103
+ expose :last_migrated_service_integration, with: Related
104
+ expose :last_migrated_timestamp
105
+ end
106
+
107
+ class OrganizationMembership < Base
108
+ expose :organization, with: Related
109
+ expose :customer, with: Related
110
+ expose :membership_role, with: Related
111
+ expose :verified
112
+ expose :invitation_code
113
+ expose :is_default
114
+ end
115
+
116
+ class Role < Base
117
+ expose :name
118
+ end
119
+
120
+ class SavedQuery < Base
121
+ expose :organization, with: Related
122
+ expose :created_by, with: Related
123
+ expose :opaque_id
124
+ expose :description
125
+ expose :sql
126
+ expose :public
127
+ end
128
+
129
+ class SavedView < Base
130
+ expose :organization, with: Related
131
+ expose :created_by, with: Related
132
+ expose :name
133
+ expose :sql
134
+ end
135
+
136
+ class ServiceIntegration < Base
137
+ expose :organization, with: Related
138
+ expose :table_name
139
+ expose :service_name
140
+ expose :opaque_id
141
+ expose :last_backfilled_at
142
+ expose :depends_on, with: Related
143
+ expose :skip_webhook_verification
144
+ end
145
+
146
+ class Subscription < Base
147
+ expose :organization, with: Related
148
+ expose :stripe_id
149
+ expose :stripe_customer_id
150
+ expose :stripe_json
151
+ end
152
+
153
+ class SyncTarget < Base
154
+ expose :organization, with: Related
155
+ expose :service_integration, with: Related
156
+ expose :created_by, with: Related
157
+ expose :opaque_id
158
+ expose :period_seconds
159
+ expose :schema
160
+ expose :table
161
+ expose :last_synced_at
162
+ expose :last_applied_schema
163
+ expose :page_size
164
+ end
165
+
166
+ class WebhookSubscription < Base
167
+ expose :organization, with: Related
168
+ expose :service_integration, with: Related
169
+ expose :created_by, with: Related
170
+ expose :opaque_id
171
+ expose :deliver_to_url
172
+ expose :deactivated_at
173
+ end
174
+
175
+ class WebhookSubscriptionDelivery < Base
176
+ expose :attempt_timestamps
177
+ expose :attempt_http_response_statuses
178
+ expose :payload
179
+ expose :webhook_subscription, with: Related
65
180
  end
66
181
  end
@@ -16,8 +16,6 @@ module Webhookdb::AdminAPI
16
16
  version "v1", using: :path
17
17
  format :json
18
18
 
19
- content_type :csv, "text/csv"
20
-
21
19
  require "webhookdb/service/helpers"
22
20
  helpers Webhookdb::Service::Helpers
23
21
 
@@ -26,13 +26,12 @@ class Webhookdb::API::Auth < Webhookdb::API::V1
26
26
 
27
27
  params do
28
28
  optional :email,
29
- type: String,
30
- coerce_with: NormalizedEmail,
29
+ type: NormalizedEmail,
31
30
  prompt: {
32
31
  message: "Welcome to WebhookDB!\nPlease enter your email:",
33
32
  demo_mode_proc: ->(r) { r.params[:email] = "demo@webhookdb.com" },
34
33
  }
35
- optional :token, type: String
34
+ optional :token, type: TrimmedString
36
35
  end
37
36
  post do
38
37
  message = ""
@@ -86,9 +85,9 @@ class Webhookdb::API::Auth < Webhookdb::API::V1
86
85
  end
87
86
 
88
87
  params do
89
- requires :form_name, type: String
90
- optional :email, type: String
91
- optional :name, type: String
88
+ requires :form_name, type: TrimmedString
89
+ optional :email, type: TrimmedString
90
+ optional :name, type: TrimmedString
92
91
  optional :message, type: String
93
92
  end
94
93
  post :contact do
@@ -6,15 +6,17 @@ require "webhookdb/api"
6
6
  require "webhookdb/replicator"
7
7
 
8
8
  class Webhookdb::API::Db < Webhookdb::API::V1
9
+ include Webhookdb::Service::Types
10
+
9
11
  helpers do
10
12
  params :fdw do
11
13
  optional :message_fdw, type: Boolean
12
14
  optional :message_views, type: Boolean
13
15
  optional :message_all, type: Boolean
14
- requires :remote_server_name, type: String
15
- requires :fetch_size, type: String
16
- requires :local_schema, type: String
17
- requires :view_schema, type: String
16
+ requires :remote_server_name, type: TrimmedString
17
+ requires :fetch_size, type: TrimmedString
18
+ requires :local_schema, type: TrimmedString
19
+ requires :view_schema, type: TrimmedString
18
20
  end
19
21
 
20
22
  def run_fdw
@@ -38,6 +40,29 @@ class Webhookdb::API::Db < Webhookdb::API::V1
38
40
  end
39
41
 
40
42
  resource :db do
43
+ [:get, :post].each do |httpmethod|
44
+ desc "Execute an arbitrary query in a replication database. Same as /db/<org>/sql but safe for CORS usage." do
45
+ headers Webhookdb::API::ConnstrAuth.headers_desc
46
+ end
47
+ params do
48
+ requires :org_identifier, type: TrimmedString
49
+ optional :query, type: String, desc: "SQL to run."
50
+ optional :query_base64, type: TrimmedString, desc: "Base64 encoded SQL. Mostly used for GET requests."
51
+ exactly_one_of :query, :query_base64
52
+ end
53
+ send(httpmethod, :run_sql) do
54
+ use_http_expires_caching(5.minutes)
55
+ org = lookup_org!(allow_connstr_auth: true)
56
+ unless (query = params[:query])
57
+ query = Base64.urlsafe_decode64(params[:query_base64])
58
+ end
59
+ r, msg = execute_readonly_query(org, query)
60
+ merror!(403, msg, code: "invalid_query") if r.nil?
61
+ status 200
62
+ present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached})
63
+ end
64
+ end
65
+
41
66
  route_param :org_identifier, type: String do
42
67
  desc "Returns the connection string"
43
68
  get :connection do
@@ -64,7 +89,7 @@ class Webhookdb::API::Db < Webhookdb::API::V1
64
89
 
65
90
  desc "Enqueues a database migration."
66
91
  params do
67
- optional :admin_url, type: String, prompt: {
92
+ optional :admin_url, type: TrimmedString, prompt: {
68
93
  message: "ADMIN Postgres connection URL, in the form 'postgres://user:password@host:port/dbname',\n" \
69
94
  "that is capable of administrative operations on your database,\n" \
70
95
  "such as creating and dropping schemas and tables.\n" \
@@ -73,7 +98,7 @@ class Webhookdb::API::Db < Webhookdb::API::V1
73
98
  disable: ->(_) { !Webhookdb::Organization::DbBuilder.allow_public_migrations },
74
99
  }
75
100
 
76
- optional :readonly_url, type: String, prompt: {
101
+ optional :readonly_url, type: TrimmedString, prompt: {
77
102
  message: "READONLY Postgres connection URL.\n" \
78
103
  "This string is displayed when you ask for your organization's connection information.\n" \
79
104
  "If you are okay with this being your ADMIN URL, leave it blank.\n" \
@@ -9,7 +9,11 @@ module Webhookdb::API
9
9
  MoneyEntity = Webhookdb::Service::Entities::Money
10
10
  TimeRangeEntity = Webhookdb::Service::Entities::TimeRange
11
11
 
12
- class BaseEntity < Webhookdb::Service::Entities::Base; end
12
+ class BaseEntity < Webhookdb::Service::Entities::Base
13
+ expose :message do |_instance, options|
14
+ options[:message] || ""
15
+ end
16
+ end
13
17
 
14
18
  class OrganizationEntity < BaseEntity
15
19
  expose :id
@@ -43,11 +47,13 @@ module Webhookdb::API
43
47
  expose :verified_memberships, with: OrganizationMembershipEntity
44
48
  expose :verified_memberships_formatted do |instance|
45
49
  lines = instance.verified_memberships.map { |m| "#{m.organization.display_string}: #{m.status}" }
50
+ lines.sort!
46
51
  lines.join("\n")
47
52
  end
48
53
  expose :invited_memberships, as: :invitations, with: OrganizationMembershipEntity
49
54
  expose :invitations_formatted do |instance|
50
55
  lines = instance.invited_memberships.map { |m| "#{m.organization.display_string}: #{m.invitation_code}" }
56
+ lines.sort!
51
57
  lines.join("\n")
52
58
  end
53
59
  expose :display_headers do |_|
@@ -164,15 +164,15 @@ module Webhookdb::API::Helpers
164
164
  request_headers = {}
165
165
  raise LocalJumpError unless block_given?
166
166
  begin
167
+ request_headers = request.headers.dup
168
+ if (content_type = env["CONTENT_TYPE"])
169
+ request_headers["Content-Type"] = content_type
170
+ end
167
171
  sint = yield
168
172
  return if sint == :pass
169
173
  raise "error instead of return nil if there is no service integration" if sint.nil?
170
174
  opaque_id = sint.opaque_id
171
175
  organization_id = sint.organization_id
172
- request_headers = request.headers.dup
173
- if (content_type = env["CONTENT_TYPE"])
174
- request_headers["Content-Type"] = content_type
175
- end
176
176
  svc = Webhookdb::Replicator.create(sint).dispatch_request_to(request)
177
177
  svc.preprocess_headers_for_logging(request_headers)
178
178
  handling_sint = svc.service_integration
@@ -205,6 +205,7 @@ module Webhookdb::API::Helpers
205
205
  path: process_kwargs[:request_path],
206
206
  headers: process_kwargs[:headers],
207
207
  body: process_kwargs[:body],
208
+ rack_request: request,
208
209
  )
209
210
  inserted = svc.upsert_webhook(whreq)
210
211
  s_body = svc.synchronous_processing_response_body(upserted: inserted, request: whreq)
@@ -256,25 +257,5 @@ module Webhookdb::API::Helpers
256
257
  # On query success, return <QueryResult, nil>.
257
258
  # On DatabaseError, return <nil, message>.
258
259
  # 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
260
+ def execute_readonly_query(org, sql) = org.execute_readonly_query_with_help(sql)
280
261
  end