webhookdb 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) 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/040_saved_query_fix_unique.rb +17 -0
  17. data/db/migrations/041_views.rb +20 -0
  18. data/db/migrations/042_sint_lock.rb +10 -0
  19. data/db/migrations/043_text_search.rb +28 -0
  20. data/db/migrations/044_oauth_session_token_cache.rb +21 -0
  21. data/integration/auth_spec.rb +2 -2
  22. data/lib/sequel/plugins/text_searchable.rb +165 -0
  23. data/lib/sequel/text_searchable.rb +42 -0
  24. data/lib/webhookdb/admin_api/auth.rb +24 -3
  25. data/lib/webhookdb/admin_api/data_provider.rb +196 -0
  26. data/lib/webhookdb/admin_api/entities.rb +143 -28
  27. data/lib/webhookdb/admin_api.rb +0 -2
  28. data/lib/webhookdb/api/auth.rb +5 -6
  29. data/lib/webhookdb/api/db.rb +31 -6
  30. data/lib/webhookdb/api/entities.rb +7 -1
  31. data/lib/webhookdb/api/helpers.rb +6 -25
  32. data/lib/webhookdb/api/install.rb +204 -79
  33. data/lib/webhookdb/api/organizations.rb +14 -12
  34. data/lib/webhookdb/api/saved_queries.rb +10 -3
  35. data/lib/webhookdb/api/saved_views.rb +99 -0
  36. data/lib/webhookdb/api/service_integrations.rb +15 -9
  37. data/lib/webhookdb/api/subscriptions.rb +3 -1
  38. data/lib/webhookdb/api/sync_targets.rb +9 -7
  39. data/lib/webhookdb/api/system.rb +1 -0
  40. data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
  41. data/lib/webhookdb/apps.rb +30 -7
  42. data/lib/webhookdb/async/audit_logger.rb +2 -0
  43. data/lib/webhookdb/async.rb +5 -0
  44. data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
  45. data/lib/webhookdb/backfill_job.rb +9 -0
  46. data/lib/webhookdb/customer.rb +5 -0
  47. data/lib/webhookdb/database_document.rb +1 -1
  48. data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
  49. data/lib/webhookdb/db_adapter.rb +20 -4
  50. data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
  51. data/lib/webhookdb/fixtures/organizations.rb +5 -0
  52. data/lib/webhookdb/fixtures/roles.rb +14 -0
  53. data/lib/webhookdb/fixtures/saved_views.rb +25 -0
  54. data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
  55. data/lib/webhookdb/http.rb +8 -2
  56. data/lib/webhookdb/icalendar.rb +3 -0
  57. data/lib/webhookdb/idempotency.rb +69 -22
  58. data/lib/webhookdb/increase.rb +69 -21
  59. data/lib/webhookdb/intercom.rb +10 -3
  60. data/lib/webhookdb/jobs/backfill.rb +3 -1
  61. data/lib/webhookdb/jobs/emailer.rb +0 -1
  62. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
  63. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
  64. data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
  65. data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
  66. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
  67. data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
  68. data/lib/webhookdb/message/body.rb +6 -4
  69. data/lib/webhookdb/message/delivery.rb +2 -0
  70. data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
  71. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
  72. data/lib/webhookdb/oauth/fake_provider.rb +44 -0
  73. data/lib/webhookdb/oauth/front_provider.rb +1 -2
  74. data/lib/webhookdb/oauth/increase_provider.rb +80 -0
  75. data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
  76. data/lib/webhookdb/oauth/session.rb +20 -0
  77. data/lib/webhookdb/oauth.rb +7 -21
  78. data/lib/webhookdb/organization/alerting.rb +2 -0
  79. data/lib/webhookdb/organization/database_migration.rb +3 -0
  80. data/lib/webhookdb/organization.rb +37 -6
  81. data/lib/webhookdb/organization_membership.rb +14 -7
  82. data/lib/webhookdb/postgres.rb +2 -0
  83. data/lib/webhookdb/replicator/base.rb +1 -0
  84. data/lib/webhookdb/replicator/docgen.rb +9 -1
  85. data/lib/webhookdb/replicator/fake.rb +2 -3
  86. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
  87. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
  88. data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
  89. data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
  90. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
  91. data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
  92. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
  93. data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
  94. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
  95. data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
  96. data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
  97. data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
  98. data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
  99. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
  100. data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
  101. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
  102. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
  103. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
  104. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
  105. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  106. data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
  107. data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
  108. data/lib/webhookdb/replicator/webhook_request.rb +4 -0
  109. data/lib/webhookdb/replicator.rb +8 -0
  110. data/lib/webhookdb/role.rb +5 -2
  111. data/lib/webhookdb/saved_query.rb +23 -0
  112. data/lib/webhookdb/saved_view.rb +73 -0
  113. data/lib/webhookdb/sentry.rb +2 -0
  114. data/lib/webhookdb/service/entities.rb +0 -4
  115. data/lib/webhookdb/service/helpers.rb +5 -0
  116. data/lib/webhookdb/service/middleware.rb +17 -0
  117. data/lib/webhookdb/service/types.rb +10 -8
  118. data/lib/webhookdb/service/validators.rb +1 -2
  119. data/lib/webhookdb/service/view_api.rb +1 -1
  120. data/lib/webhookdb/service_integration.rb +17 -15
  121. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
  122. data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
  123. data/lib/webhookdb/subscription.rb +2 -0
  124. data/lib/webhookdb/sync_target.rb +10 -2
  125. data/lib/webhookdb/tasks/message.rb +3 -1
  126. data/lib/webhookdb/version.rb +1 -1
  127. data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
  128. data/lib/webhookdb/webhook_subscription.rb +2 -0
  129. metadata +58 -9
  130. data/lib/webhookdb/admin_api/customers.rb +0 -63
  131. data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
  132. 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