webhookdb 0.1.0

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 (364) hide show
  1. checksums.yaml +7 -0
  2. data/data/messages/layouts/blank.email.liquid +10 -0
  3. data/data/messages/layouts/minimal.email.liquid +28 -0
  4. data/data/messages/layouts/standard.email.liquid +28 -0
  5. data/data/messages/partials/button.liquid +15 -0
  6. data/data/messages/partials/environment_banner.liquid +9 -0
  7. data/data/messages/partials/footer.liquid +22 -0
  8. data/data/messages/partials/greeting.liquid +3 -0
  9. data/data/messages/partials/logo_header.liquid +18 -0
  10. data/data/messages/partials/signoff.liquid +1 -0
  11. data/data/messages/styles/v1.liquid +346 -0
  12. data/data/messages/templates/errors/icalendar_fetch.email.liquid +29 -0
  13. data/data/messages/templates/invite.email.liquid +15 -0
  14. data/data/messages/templates/new_customer.email.liquid +24 -0
  15. data/data/messages/templates/org_database_migration_finished.email.liquid +7 -0
  16. data/data/messages/templates/org_database_migration_started.email.liquid +9 -0
  17. data/data/messages/templates/specs/_field_partial.liquid +1 -0
  18. data/data/messages/templates/specs/basic.email.liquid +2 -0
  19. data/data/messages/templates/specs/basic.fake.liquid +1 -0
  20. data/data/messages/templates/specs/with_field.email.liquid +2 -0
  21. data/data/messages/templates/specs/with_field.fake.liquid +1 -0
  22. data/data/messages/templates/specs/with_include.email.liquid +2 -0
  23. data/data/messages/templates/specs/with_partial.email.liquid +1 -0
  24. data/data/messages/templates/verification.email.liquid +14 -0
  25. data/data/messages/templates/verification.sms.liquid +1 -0
  26. data/data/messages/web/install-customer-login.liquid +48 -0
  27. data/data/messages/web/install-error.liquid +17 -0
  28. data/data/messages/web/install-success.liquid +35 -0
  29. data/data/messages/web/install.liquid +20 -0
  30. data/data/messages/web/partials/footer.liquid +4 -0
  31. data/data/messages/web/partials/form_error.liquid +1 -0
  32. data/data/messages/web/partials/header.liquid +3 -0
  33. data/data/messages/web/styles.liquid +134 -0
  34. data/data/windows_tz.txt +461 -0
  35. data/db/migrations/001_testing_pixies.rb +13 -0
  36. data/db/migrations/002_initial.rb +132 -0
  37. data/db/migrations/003_ux_overhaul.rb +20 -0
  38. data/db/migrations/004_incremental_backfill.rb +9 -0
  39. data/db/migrations/005_log_webhooks.rb +24 -0
  40. data/db/migrations/006_generalize_roles.rb +29 -0
  41. data/db/migrations/007_org_dns.rb +12 -0
  42. data/db/migrations/008_webhook_subscriptions.rb +19 -0
  43. data/db/migrations/009_nonunique_stripe_subscription_customer.rb +16 -0
  44. data/db/migrations/010_drop_integration_soft_delete.rb +14 -0
  45. data/db/migrations/011_webhook_subscriptions_created_at.rb +10 -0
  46. data/db/migrations/012_webhook_subscriptions_created_by.rb +9 -0
  47. data/db/migrations/013_default_org_membership.rb +30 -0
  48. data/db/migrations/014_webhook_subscription_deliveries.rb +26 -0
  49. data/db/migrations/015_dependent_integrations.rb +9 -0
  50. data/db/migrations/016_encrypted_columns.rb +9 -0
  51. data/db/migrations/017_skip_verification.rb +9 -0
  52. data/db/migrations/018_sync_targets.rb +25 -0
  53. data/db/migrations/019_org_schema.rb +9 -0
  54. data/db/migrations/020_org_database_migrations.rb +25 -0
  55. data/db/migrations/021_no_default_org_schema.rb +14 -0
  56. data/db/migrations/022_database_document.rb +15 -0
  57. data/db/migrations/023_sync_target_schema.rb +9 -0
  58. data/db/migrations/024_org_semaphore_jobs.rb +9 -0
  59. data/db/migrations/025_integration_backfill_cursor.rb +9 -0
  60. data/db/migrations/026_undo_integration_backfill_cursor.rb +9 -0
  61. data/db/migrations/027_sync_target_http_sync.rb +12 -0
  62. data/db/migrations/028_logged_webhook_path.rb +24 -0
  63. data/db/migrations/029_encrypt_columns.rb +97 -0
  64. data/db/migrations/030_org_sync_target_timeout.rb +9 -0
  65. data/db/migrations/031_org_max_query_rows.rb +9 -0
  66. data/db/migrations/032_remove_db_defaults.rb +12 -0
  67. data/db/migrations/033_backfill_jobs.rb +26 -0
  68. data/db/migrations/034_backfill_job_criteria.rb +9 -0
  69. data/db/migrations/035_synchronous_backfill.rb +9 -0
  70. data/db/migrations/036_oauth.rb +26 -0
  71. data/db/migrations/037_oauth_used.rb +9 -0
  72. data/lib/amigo/durable_job.rb +416 -0
  73. data/lib/pry/clipboard.rb +111 -0
  74. data/lib/sequel/advisory_lock.rb +65 -0
  75. data/lib/webhookdb/admin.rb +4 -0
  76. data/lib/webhookdb/admin_api/auth.rb +36 -0
  77. data/lib/webhookdb/admin_api/customers.rb +63 -0
  78. data/lib/webhookdb/admin_api/database_documents.rb +20 -0
  79. data/lib/webhookdb/admin_api/entities.rb +66 -0
  80. data/lib/webhookdb/admin_api/message_deliveries.rb +61 -0
  81. data/lib/webhookdb/admin_api/roles.rb +15 -0
  82. data/lib/webhookdb/admin_api.rb +34 -0
  83. data/lib/webhookdb/aggregate_result.rb +63 -0
  84. data/lib/webhookdb/api/auth.rb +122 -0
  85. data/lib/webhookdb/api/connstr_auth.rb +36 -0
  86. data/lib/webhookdb/api/db.rb +188 -0
  87. data/lib/webhookdb/api/demo.rb +14 -0
  88. data/lib/webhookdb/api/entities.rb +198 -0
  89. data/lib/webhookdb/api/helpers.rb +253 -0
  90. data/lib/webhookdb/api/install.rb +296 -0
  91. data/lib/webhookdb/api/me.rb +53 -0
  92. data/lib/webhookdb/api/organizations.rb +254 -0
  93. data/lib/webhookdb/api/replay.rb +64 -0
  94. data/lib/webhookdb/api/service_integrations.rb +402 -0
  95. data/lib/webhookdb/api/services.rb +27 -0
  96. data/lib/webhookdb/api/stripe.rb +22 -0
  97. data/lib/webhookdb/api/subscriptions.rb +67 -0
  98. data/lib/webhookdb/api/sync_targets.rb +232 -0
  99. data/lib/webhookdb/api/system.rb +37 -0
  100. data/lib/webhookdb/api/webhook_subscriptions.rb +96 -0
  101. data/lib/webhookdb/api.rb +92 -0
  102. data/lib/webhookdb/apps.rb +93 -0
  103. data/lib/webhookdb/async/audit_logger.rb +38 -0
  104. data/lib/webhookdb/async/autoscaler.rb +84 -0
  105. data/lib/webhookdb/async/job.rb +18 -0
  106. data/lib/webhookdb/async/job_logger.rb +45 -0
  107. data/lib/webhookdb/async/scheduled_job.rb +18 -0
  108. data/lib/webhookdb/async.rb +142 -0
  109. data/lib/webhookdb/aws.rb +98 -0
  110. data/lib/webhookdb/backfill_job.rb +107 -0
  111. data/lib/webhookdb/backfiller.rb +107 -0
  112. data/lib/webhookdb/cloudflare.rb +39 -0
  113. data/lib/webhookdb/connection_cache.rb +177 -0
  114. data/lib/webhookdb/console.rb +71 -0
  115. data/lib/webhookdb/convertkit.rb +14 -0
  116. data/lib/webhookdb/crypto.rb +66 -0
  117. data/lib/webhookdb/customer/reset_code.rb +94 -0
  118. data/lib/webhookdb/customer.rb +347 -0
  119. data/lib/webhookdb/database_document.rb +72 -0
  120. data/lib/webhookdb/db_adapter/column_types.rb +37 -0
  121. data/lib/webhookdb/db_adapter/default_sql.rb +187 -0
  122. data/lib/webhookdb/db_adapter/pg.rb +96 -0
  123. data/lib/webhookdb/db_adapter/snowflake.rb +137 -0
  124. data/lib/webhookdb/db_adapter.rb +208 -0
  125. data/lib/webhookdb/dbutil.rb +92 -0
  126. data/lib/webhookdb/demo_mode.rb +100 -0
  127. data/lib/webhookdb/developer_alert.rb +51 -0
  128. data/lib/webhookdb/email_octopus.rb +21 -0
  129. data/lib/webhookdb/enumerable.rb +18 -0
  130. data/lib/webhookdb/fixtures/backfill_jobs.rb +72 -0
  131. data/lib/webhookdb/fixtures/customers.rb +65 -0
  132. data/lib/webhookdb/fixtures/database_documents.rb +27 -0
  133. data/lib/webhookdb/fixtures/faker.rb +41 -0
  134. data/lib/webhookdb/fixtures/logged_webhooks.rb +56 -0
  135. data/lib/webhookdb/fixtures/message_deliveries.rb +59 -0
  136. data/lib/webhookdb/fixtures/oauth_sessions.rb +24 -0
  137. data/lib/webhookdb/fixtures/organization_database_migrations.rb +37 -0
  138. data/lib/webhookdb/fixtures/organization_memberships.rb +54 -0
  139. data/lib/webhookdb/fixtures/organizations.rb +32 -0
  140. data/lib/webhookdb/fixtures/reset_codes.rb +23 -0
  141. data/lib/webhookdb/fixtures/service_integrations.rb +42 -0
  142. data/lib/webhookdb/fixtures/subscriptions.rb +33 -0
  143. data/lib/webhookdb/fixtures/sync_targets.rb +32 -0
  144. data/lib/webhookdb/fixtures/webhook_subscriptions.rb +35 -0
  145. data/lib/webhookdb/fixtures.rb +15 -0
  146. data/lib/webhookdb/formatting.rb +56 -0
  147. data/lib/webhookdb/front.rb +49 -0
  148. data/lib/webhookdb/github.rb +22 -0
  149. data/lib/webhookdb/google_calendar.rb +29 -0
  150. data/lib/webhookdb/heroku.rb +21 -0
  151. data/lib/webhookdb/http.rb +114 -0
  152. data/lib/webhookdb/icalendar.rb +17 -0
  153. data/lib/webhookdb/id.rb +17 -0
  154. data/lib/webhookdb/idempotency.rb +90 -0
  155. data/lib/webhookdb/increase.rb +42 -0
  156. data/lib/webhookdb/intercom.rb +23 -0
  157. data/lib/webhookdb/jobs/amigo_test_jobs.rb +118 -0
  158. data/lib/webhookdb/jobs/backfill.rb +32 -0
  159. data/lib/webhookdb/jobs/create_mirror_table.rb +18 -0
  160. data/lib/webhookdb/jobs/create_stripe_customer.rb +17 -0
  161. data/lib/webhookdb/jobs/customer_created_notify_internal.rb +22 -0
  162. data/lib/webhookdb/jobs/demo_mode_sync_data.rb +19 -0
  163. data/lib/webhookdb/jobs/deprecated_jobs.rb +19 -0
  164. data/lib/webhookdb/jobs/developer_alert_handle.rb +14 -0
  165. data/lib/webhookdb/jobs/durable_job_recheck_poller.rb +17 -0
  166. data/lib/webhookdb/jobs/emailer.rb +15 -0
  167. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +25 -0
  168. data/lib/webhookdb/jobs/icalendar_sync.rb +23 -0
  169. data/lib/webhookdb/jobs/logged_webhook_replay.rb +17 -0
  170. data/lib/webhookdb/jobs/logged_webhook_resilient_replay.rb +15 -0
  171. data/lib/webhookdb/jobs/message_dispatched.rb +16 -0
  172. data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +21 -0
  173. data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +21 -0
  174. data/lib/webhookdb/jobs/organization_database_migration_run.rb +24 -0
  175. data/lib/webhookdb/jobs/prepare_database_connections.rb +22 -0
  176. data/lib/webhookdb/jobs/process_webhook.rb +47 -0
  177. data/lib/webhookdb/jobs/renew_watch_channel.rb +24 -0
  178. data/lib/webhookdb/jobs/replication_migration.rb +24 -0
  179. data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +23 -0
  180. data/lib/webhookdb/jobs/scheduled_backfills.rb +77 -0
  181. data/lib/webhookdb/jobs/send_invite.rb +15 -0
  182. data/lib/webhookdb/jobs/send_test_webhook.rb +25 -0
  183. data/lib/webhookdb/jobs/send_webhook.rb +20 -0
  184. data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +16 -0
  185. data/lib/webhookdb/jobs/sync_target_run_sync.rb +38 -0
  186. data/lib/webhookdb/jobs/trim_logged_webhooks.rb +15 -0
  187. data/lib/webhookdb/jobs/webhook_resource_notify_integrations.rb +30 -0
  188. data/lib/webhookdb/jobs/webhook_subscription_delivery_attempt.rb +29 -0
  189. data/lib/webhookdb/jobs.rb +4 -0
  190. data/lib/webhookdb/json.rb +113 -0
  191. data/lib/webhookdb/liquid/expose.rb +27 -0
  192. data/lib/webhookdb/liquid/filters.rb +16 -0
  193. data/lib/webhookdb/liquid/liquification.rb +26 -0
  194. data/lib/webhookdb/liquid/partial.rb +12 -0
  195. data/lib/webhookdb/logged_webhook/resilient.rb +95 -0
  196. data/lib/webhookdb/logged_webhook.rb +194 -0
  197. data/lib/webhookdb/message/body.rb +25 -0
  198. data/lib/webhookdb/message/delivery.rb +127 -0
  199. data/lib/webhookdb/message/email_transport.rb +133 -0
  200. data/lib/webhookdb/message/fake_transport.rb +54 -0
  201. data/lib/webhookdb/message/liquid_drops.rb +29 -0
  202. data/lib/webhookdb/message/template.rb +89 -0
  203. data/lib/webhookdb/message/transport.rb +43 -0
  204. data/lib/webhookdb/message.rb +150 -0
  205. data/lib/webhookdb/messages/error_icalendar_fetch.rb +42 -0
  206. data/lib/webhookdb/messages/invite.rb +23 -0
  207. data/lib/webhookdb/messages/new_customer.rb +14 -0
  208. data/lib/webhookdb/messages/org_database_migration_finished.rb +23 -0
  209. data/lib/webhookdb/messages/org_database_migration_started.rb +24 -0
  210. data/lib/webhookdb/messages/specs.rb +57 -0
  211. data/lib/webhookdb/messages/verification.rb +23 -0
  212. data/lib/webhookdb/method_utilities.rb +82 -0
  213. data/lib/webhookdb/microsoft_calendar.rb +36 -0
  214. data/lib/webhookdb/nextpax.rb +14 -0
  215. data/lib/webhookdb/oauth/front.rb +58 -0
  216. data/lib/webhookdb/oauth/intercom.rb +58 -0
  217. data/lib/webhookdb/oauth/session.rb +24 -0
  218. data/lib/webhookdb/oauth.rb +80 -0
  219. data/lib/webhookdb/organization/alerting.rb +35 -0
  220. data/lib/webhookdb/organization/database_migration.rb +151 -0
  221. data/lib/webhookdb/organization/db_builder.rb +429 -0
  222. data/lib/webhookdb/organization.rb +506 -0
  223. data/lib/webhookdb/organization_membership.rb +58 -0
  224. data/lib/webhookdb/phone_number.rb +38 -0
  225. data/lib/webhookdb/plaid.rb +23 -0
  226. data/lib/webhookdb/platform.rb +27 -0
  227. data/lib/webhookdb/plivo.rb +52 -0
  228. data/lib/webhookdb/postgres/maintenance.rb +166 -0
  229. data/lib/webhookdb/postgres/model.rb +82 -0
  230. data/lib/webhookdb/postgres/model_utilities.rb +382 -0
  231. data/lib/webhookdb/postgres/testing_pixie.rb +16 -0
  232. data/lib/webhookdb/postgres/validations.rb +46 -0
  233. data/lib/webhookdb/postgres.rb +176 -0
  234. data/lib/webhookdb/postmark.rb +20 -0
  235. data/lib/webhookdb/redis.rb +35 -0
  236. data/lib/webhookdb/replicator/atom_single_feed_v1.rb +116 -0
  237. data/lib/webhookdb/replicator/aws_pricing_v1.rb +488 -0
  238. data/lib/webhookdb/replicator/base.rb +1185 -0
  239. data/lib/webhookdb/replicator/column.rb +482 -0
  240. data/lib/webhookdb/replicator/convertkit_broadcast_v1.rb +69 -0
  241. data/lib/webhookdb/replicator/convertkit_subscriber_v1.rb +200 -0
  242. data/lib/webhookdb/replicator/convertkit_tag_v1.rb +66 -0
  243. data/lib/webhookdb/replicator/convertkit_v1_mixin.rb +65 -0
  244. data/lib/webhookdb/replicator/docgen.rb +167 -0
  245. data/lib/webhookdb/replicator/email_octopus_campaign_v1.rb +84 -0
  246. data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +159 -0
  247. data/lib/webhookdb/replicator/email_octopus_event_v1.rb +244 -0
  248. data/lib/webhookdb/replicator/email_octopus_list_v1.rb +101 -0
  249. data/lib/webhookdb/replicator/fake.rb +453 -0
  250. data/lib/webhookdb/replicator/front_conversation_v1.rb +45 -0
  251. data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +55 -0
  252. data/lib/webhookdb/replicator/front_message_v1.rb +45 -0
  253. data/lib/webhookdb/replicator/front_v1_mixin.rb +22 -0
  254. data/lib/webhookdb/replicator/github_issue_comment_v1.rb +58 -0
  255. data/lib/webhookdb/replicator/github_issue_v1.rb +83 -0
  256. data/lib/webhookdb/replicator/github_pull_v1.rb +84 -0
  257. data/lib/webhookdb/replicator/github_release_v1.rb +47 -0
  258. data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +250 -0
  259. data/lib/webhookdb/replicator/github_repository_event_v1.rb +45 -0
  260. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +465 -0
  261. data/lib/webhookdb/replicator/icalendar_event_v1.rb +334 -0
  262. data/lib/webhookdb/replicator/increase_account_number_v1.rb +77 -0
  263. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +61 -0
  264. data/lib/webhookdb/replicator/increase_account_v1.rb +63 -0
  265. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +78 -0
  266. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +64 -0
  267. data/lib/webhookdb/replicator/increase_limit_v1.rb +78 -0
  268. data/lib/webhookdb/replicator/increase_transaction_v1.rb +74 -0
  269. data/lib/webhookdb/replicator/increase_v1_mixin.rb +121 -0
  270. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +61 -0
  271. data/lib/webhookdb/replicator/intercom_contact_v1.rb +36 -0
  272. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +38 -0
  273. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +69 -0
  274. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +105 -0
  275. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +65 -0
  276. data/lib/webhookdb/replicator/plivo_sms_inbound_v1.rb +102 -0
  277. data/lib/webhookdb/replicator/postmark_inbound_message_v1.rb +94 -0
  278. data/lib/webhookdb/replicator/postmark_outbound_message_event_v1.rb +107 -0
  279. data/lib/webhookdb/replicator/schema_modification.rb +42 -0
  280. data/lib/webhookdb/replicator/shopify_customer_v1.rb +58 -0
  281. data/lib/webhookdb/replicator/shopify_order_v1.rb +64 -0
  282. data/lib/webhookdb/replicator/shopify_v1_mixin.rb +161 -0
  283. data/lib/webhookdb/replicator/signalwire_message_v1.rb +169 -0
  284. data/lib/webhookdb/replicator/sponsy_customer_v1.rb +54 -0
  285. data/lib/webhookdb/replicator/sponsy_placement_v1.rb +34 -0
  286. data/lib/webhookdb/replicator/sponsy_publication_v1.rb +125 -0
  287. data/lib/webhookdb/replicator/sponsy_slot_v1.rb +41 -0
  288. data/lib/webhookdb/replicator/sponsy_status_v1.rb +35 -0
  289. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +165 -0
  290. data/lib/webhookdb/replicator/state_machine_step.rb +69 -0
  291. data/lib/webhookdb/replicator/stripe_charge_v1.rb +77 -0
  292. data/lib/webhookdb/replicator/stripe_coupon_v1.rb +62 -0
  293. data/lib/webhookdb/replicator/stripe_customer_v1.rb +60 -0
  294. data/lib/webhookdb/replicator/stripe_dispute_v1.rb +77 -0
  295. data/lib/webhookdb/replicator/stripe_invoice_item_v1.rb +82 -0
  296. data/lib/webhookdb/replicator/stripe_invoice_v1.rb +116 -0
  297. data/lib/webhookdb/replicator/stripe_payout_v1.rb +67 -0
  298. data/lib/webhookdb/replicator/stripe_price_v1.rb +60 -0
  299. data/lib/webhookdb/replicator/stripe_product_v1.rb +60 -0
  300. data/lib/webhookdb/replicator/stripe_refund_v1.rb +101 -0
  301. data/lib/webhookdb/replicator/stripe_subscription_item_v1.rb +56 -0
  302. data/lib/webhookdb/replicator/stripe_subscription_v1.rb +75 -0
  303. data/lib/webhookdb/replicator/stripe_v1_mixin.rb +116 -0
  304. data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +141 -0
  305. data/lib/webhookdb/replicator/transistor_episode_v1.rb +169 -0
  306. data/lib/webhookdb/replicator/transistor_show_v1.rb +68 -0
  307. data/lib/webhookdb/replicator/transistor_v1_mixin.rb +65 -0
  308. data/lib/webhookdb/replicator/twilio_sms_v1.rb +156 -0
  309. data/lib/webhookdb/replicator/webhook_request.rb +5 -0
  310. data/lib/webhookdb/replicator/webhookdb_customer_v1.rb +74 -0
  311. data/lib/webhookdb/replicator.rb +224 -0
  312. data/lib/webhookdb/role.rb +42 -0
  313. data/lib/webhookdb/sentry.rb +35 -0
  314. data/lib/webhookdb/service/auth.rb +138 -0
  315. data/lib/webhookdb/service/collection.rb +91 -0
  316. data/lib/webhookdb/service/entities.rb +97 -0
  317. data/lib/webhookdb/service/helpers.rb +270 -0
  318. data/lib/webhookdb/service/middleware.rb +124 -0
  319. data/lib/webhookdb/service/types.rb +30 -0
  320. data/lib/webhookdb/service/validators.rb +32 -0
  321. data/lib/webhookdb/service/view_api.rb +63 -0
  322. data/lib/webhookdb/service.rb +219 -0
  323. data/lib/webhookdb/service_integration.rb +332 -0
  324. data/lib/webhookdb/shopify.rb +35 -0
  325. data/lib/webhookdb/signalwire.rb +13 -0
  326. data/lib/webhookdb/slack.rb +68 -0
  327. data/lib/webhookdb/snowflake.rb +90 -0
  328. data/lib/webhookdb/spec_helpers/async.rb +122 -0
  329. data/lib/webhookdb/spec_helpers/citest.rb +88 -0
  330. data/lib/webhookdb/spec_helpers/integration.rb +121 -0
  331. data/lib/webhookdb/spec_helpers/message.rb +41 -0
  332. data/lib/webhookdb/spec_helpers/postgres.rb +220 -0
  333. data/lib/webhookdb/spec_helpers/service.rb +432 -0
  334. data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +56 -0
  335. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +915 -0
  336. data/lib/webhookdb/spec_helpers/whdb.rb +139 -0
  337. data/lib/webhookdb/spec_helpers.rb +63 -0
  338. data/lib/webhookdb/sponsy.rb +14 -0
  339. data/lib/webhookdb/stripe.rb +37 -0
  340. data/lib/webhookdb/subscription.rb +203 -0
  341. data/lib/webhookdb/sync_target.rb +491 -0
  342. data/lib/webhookdb/tasks/admin.rb +49 -0
  343. data/lib/webhookdb/tasks/annotate.rb +36 -0
  344. data/lib/webhookdb/tasks/db.rb +82 -0
  345. data/lib/webhookdb/tasks/docs.rb +42 -0
  346. data/lib/webhookdb/tasks/fixture.rb +35 -0
  347. data/lib/webhookdb/tasks/message.rb +50 -0
  348. data/lib/webhookdb/tasks/regress.rb +87 -0
  349. data/lib/webhookdb/tasks/release.rb +27 -0
  350. data/lib/webhookdb/tasks/sidekiq.rb +23 -0
  351. data/lib/webhookdb/tasks/specs.rb +64 -0
  352. data/lib/webhookdb/theranest.rb +15 -0
  353. data/lib/webhookdb/transistor.rb +13 -0
  354. data/lib/webhookdb/twilio.rb +13 -0
  355. data/lib/webhookdb/typed_struct.rb +44 -0
  356. data/lib/webhookdb/version.rb +5 -0
  357. data/lib/webhookdb/webhook_response.rb +50 -0
  358. data/lib/webhookdb/webhook_subscription/delivery.rb +82 -0
  359. data/lib/webhookdb/webhook_subscription.rb +226 -0
  360. data/lib/webhookdb/windows_tz.rb +32 -0
  361. data/lib/webhookdb/xml.rb +92 -0
  362. data/lib/webhookdb.rb +224 -0
  363. data/lib/webterm/apps.rb +45 -0
  364. metadata +1129 -0
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/email_octopus"
4
+
5
+ class Webhookdb::Replicator::EmailOctopusContactV1 < Webhookdb::Replicator::Base
6
+ include Appydays::Loggable
7
+
8
+ # @return [Webhookdb::Replicator::Descriptor]
9
+ def self.descriptor
10
+ return Webhookdb::Replicator::Descriptor.new(
11
+ name: "email_octopus_contact_v1",
12
+ ctor: ->(sint) { Webhookdb::Replicator::EmailOctopusContactV1.new(sint) },
13
+ feature_roles: [],
14
+ resource_name_singular: "Email Octopus Contact",
15
+ dependency_descriptor: Webhookdb::Replicator::EmailOctopusListV1.descriptor,
16
+ supports_backfill: true,
17
+ api_docs_url: "https://emailoctopus.com/api-documentation",
18
+ )
19
+ end
20
+
21
+ CONV_REMOTE_KEY = Webhookdb::Replicator::Column::IsomorphicProc.new(
22
+ ruby: ->(_, resource:, **_) { "#{resource.fetch('id')}-#{resource.fetch('list_id')}" },
23
+ # Because this is a non-nullable key, we never need this in SQL
24
+ sql: ->(_) { Sequel.lit("'do not use'") },
25
+ )
26
+
27
+ def _remote_key_column
28
+ return Webhookdb::Replicator::Column.new(
29
+ :compound_identity,
30
+ TEXT,
31
+ data_key: "<compound key, see converter>",
32
+ index: true,
33
+ optional: true,
34
+ converter: CONV_REMOTE_KEY,
35
+ )
36
+ end
37
+
38
+ def _denormalized_columns
39
+ return [
40
+ Webhookdb::Replicator::Column.new(:email_octopus_id, TEXT, data_key: "id"),
41
+ Webhookdb::Replicator::Column.new(:email_octopus_list_id, TEXT, data_key: "list_id"),
42
+ Webhookdb::Replicator::Column.new(:email_address, TEXT),
43
+ Webhookdb::Replicator::Column.new(:status, TEXT),
44
+ Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, index: true, converter: :time, skip_nil: true),
45
+ Webhookdb::Replicator::Column.new(:deleted_at, TIMESTAMP, converter: :time, optional: true),
46
+ Webhookdb::Replicator::Column.new(:row_updated_at, TIMESTAMP, defaulter: :now, optional: true),
47
+ ]
48
+ end
49
+
50
+ def _upsert_webhook(request, upsert: true)
51
+ return super unless request.body.is_a?(Array)
52
+
53
+ # If the body is an array, it means we are upserting data from webhooks, which has been passed to this function by
54
+ # the event replicator, and we have to transform the data in order to be able to upsert.
55
+ new_bodies = request.body.
56
+ # Events older than 30 days may not have occurred_at on free plans
57
+ select { |wh| wh.key?("occurred_at") }.
58
+ map do |wh|
59
+ event_type = wh.fetch("type")
60
+ {
61
+ "id" => wh.fetch("contact_id"),
62
+ "list_id" => wh.fetch("list_id"),
63
+ "email_address" => wh.fetch("contact_email_address"),
64
+ "status" => wh.fetch("contact_status"),
65
+ "row_updated_at" => wh.fetch("occurred_at"),
66
+ "created_at" => event_type == "contact.created" ? wh.fetch("occurred_at") : nil,
67
+ "deleted_at" => event_type == "contact.deleted" ? wh.fetch("occurred_at") : nil,
68
+ # These fields do not get denormalized but we still want the info
69
+ # to be present in the "data" field of the row.
70
+ "fields" => wh["contact_fields"],
71
+ "tags" => wh["contact_tags"],
72
+ }
73
+ end
74
+ new_bodies.each do |b|
75
+ new_request = request.dup
76
+ new_request.body = b
77
+ super(new_request, upsert:)
78
+ end
79
+ end
80
+
81
+ def _resource_and_event(request)
82
+ return request.body, nil
83
+ end
84
+
85
+ def _update_where_expr
86
+ return self.qualified_table_sequel_identifier[:data] !~ Sequel[:excluded][:data]
87
+ end
88
+
89
+ def _timestamp_column_name
90
+ return :row_updated_at
91
+ end
92
+
93
+ def _webhook_response(_request)
94
+ return Webhookdb::WebhookResponse.ok
95
+ end
96
+
97
+ def calculate_backfill_state_machine
98
+ if (step = self.calculate_dependency_state_machine_step(dependency_help: ""))
99
+ return step
100
+ end
101
+ step = Webhookdb::Replicator::StateMachineStep.new
102
+ # We're using the API Key from the dependency, we don't need to ask for it here
103
+ step.output = %(Great! We are going to start replicating your #{self.resource_name_plural}.
104
+ #{self._query_help_output}
105
+ )
106
+ return step.completed
107
+ end
108
+
109
+ def on_dependency_webhook_upsert(_replicator, _payload, *)
110
+ return
111
+ end
112
+
113
+ def _backfillers
114
+ list_sint = self.service_integration.depends_on
115
+ api_key = list_sint.replicator.backfill_key!
116
+ backfillers = list_sint.replicator.admin_dataset(timeout: :fast) do |list_ds|
117
+ list_ds.select(:email_octopus_id).map do |list|
118
+ ContactBackfiller.new(
119
+ contact_svc: self,
120
+ list_id: list[:email_octopus_id],
121
+ api_key:,
122
+ )
123
+ end
124
+ end
125
+ return backfillers
126
+ end
127
+
128
+ class ContactBackfiller < Webhookdb::Backfiller
129
+ include Webhookdb::Backfiller::Bulk
130
+
131
+ def initialize(contact_svc:, list_id:, api_key:)
132
+ @contact_svc = contact_svc
133
+ @list_id = list_id
134
+ @api_key = api_key
135
+ super()
136
+ end
137
+
138
+ def upserting_replicator = @contact_svc
139
+ def upsert_page_size = 500
140
+
141
+ def prepare_body(body)
142
+ body["list_id"] = @list_id
143
+ end
144
+
145
+ def fetch_backfill_page(pagination_token, **_kwargs)
146
+ limit = Webhookdb::EmailOctopus.page_size
147
+ base_url = "https://emailoctopus.com"
148
+ endpoint_path = pagination_token || "/api/1.6/lists/#{@list_id}/contacts?api_key=#{@api_key}&limit=#{limit}"
149
+ response = Webhookdb::Http.get(
150
+ base_url + endpoint_path,
151
+ logger: @contact_svc.logger,
152
+ timeout: Webhookdb::EmailOctopus.http_timeout,
153
+ )
154
+ data = response.parsed_response
155
+ next_page_link = data.dig("paging", "next")
156
+ return data["data"], next_page_link
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/email_octopus"
4
+
5
+ class Webhookdb::Replicator::EmailOctopusEventV1 < Webhookdb::Replicator::Base
6
+ include Appydays::Loggable
7
+
8
+ # @return [Webhookdb::Replicator::Descriptor]
9
+ def self.descriptor
10
+ return Webhookdb::Replicator::Descriptor.new(
11
+ name: "email_octopus_event_v1",
12
+ ctor: ->(sint) { Webhookdb::Replicator::EmailOctopusEventV1.new(sint) },
13
+ feature_roles: [],
14
+ resource_name_singular: "Email Octopus Event",
15
+ dependency_descriptor: Webhookdb::Replicator::EmailOctopusListV1.descriptor,
16
+ supports_webhooks: true,
17
+ supports_backfill: true,
18
+ api_docs_url: "https://emailoctopus.com/api-documentation",
19
+ )
20
+ end
21
+ BUILD_EVENT_MD5 = Webhookdb::Replicator::Column::IsomorphicProc.new(
22
+ ruby: lambda do |resource:, **|
23
+ # MD5 includes occurred_at, event_type, email_octopus_contact_id, and email_octopus_campaign_id.
24
+ md5 = Digest::MD5.new
25
+ md5.update(resource.fetch("occurred_at"))
26
+ md5.update(resource.fetch("event_type"))
27
+ md5.update(resource.dig("contact", "id"))
28
+ md5.update(resource.fetch("campaign_id", "missing"))
29
+ md5.hexdigest
30
+ end,
31
+ )
32
+
33
+ def _remote_key_column
34
+ return Webhookdb::Replicator::Column.new(:unique_id, UUID, optional: true, defaulter: BUILD_EVENT_MD5)
35
+ end
36
+
37
+ def _denormalized_columns
38
+ return [
39
+ Webhookdb::Replicator::Column.new(:email_octopus_contact_id, TEXT, data_key: ["contact", "id"]),
40
+ Webhookdb::Replicator::Column.new(:contact_email_address, TEXT, data_key: ["contact", "email_address"]),
41
+ Webhookdb::Replicator::Column.new(:email_octopus_campaign_id, TEXT, data_key: ["campaign_id"], optional: true),
42
+ Webhookdb::Replicator::Column.new(:event_type, TEXT),
43
+ Webhookdb::Replicator::Column.new(:occurred_at, TIMESTAMP, index: true, converter: :time),
44
+ ]
45
+ end
46
+
47
+ def _timestamp_column_name
48
+ return :occurred_at
49
+ end
50
+
51
+ def _upsert_webhook(request, upsert: true)
52
+ return super unless request.body.is_a?(Array)
53
+
54
+ # If the body is an array, it means we are upserting data from webhooks and have to transform the data
55
+ # in order to be able to upsert.
56
+ new_bodies = request.body.
57
+ # Events older than 30 days may not have occurred_at on free plans
58
+ select { |wh| wh.key?("occurred_at") }.
59
+ map do |wh|
60
+ new_body = {
61
+ "contact" => {
62
+ "id" => wh.fetch("contact_id"),
63
+ "email_address" => wh.fetch("contact_email_address"),
64
+ },
65
+ "occurred_at" => wh.fetch("occurred_at"),
66
+ "event_type" => wh.fetch("type"),
67
+ }
68
+
69
+ # "campaign_id" isn't always populated in the webhoooks, it is only there on event types that are tied
70
+ # to a specific "campaign", (that's Email Octopus's word for an email message), like "bounced" or "opened"
71
+ if (campaign_id = wh["campaign_id"])
72
+ new_body["campaign_id"] = campaign_id
73
+ end
74
+ new_body
75
+ end
76
+
77
+ new_bodies.each do |b|
78
+ new_request = request.dup
79
+ new_request.body = b
80
+ super(new_request, upsert:)
81
+ end
82
+
83
+ list_svc = self.service_integration.depends_on.replicator
84
+ contact_sint = list_svc.find_dependent("email_octopus_contact_v1")
85
+ return unless contact_sint
86
+ contact_svc = contact_sint.replicator
87
+ # For events that pertain to a contact being created or updated in some way, we also upsert them using
88
+ # the contact integration so that the new information can be recorded in the contact table
89
+ contact_event_types = ["contact.created", "contact.updated", "contact.deleted"]
90
+ contact_events = request.body.filter { |nb| contact_event_types.include?(nb.fetch("type")) }
91
+ return if contact_events.empty?
92
+ contact_request = request.dup
93
+ contact_request.body = contact_events
94
+ contact_svc._upsert_webhook(contact_request, upsert:)
95
+ end
96
+
97
+ def _resource_and_event(request)
98
+ return nil, nil if request.body.is_a?(Hash) && !request.body.key?("occurred_at")
99
+ return request.body, nil
100
+ end
101
+
102
+ def _update_where_expr
103
+ # 'occurred_at' is the timestamp, but it's used in the unique id,
104
+ # so there's no way to use it as part of the conditional upsert.
105
+ # So events are effectively immutable, and we know we shouldn't bother overwriting what's already in the DB.
106
+ return Sequel[false]
107
+ end
108
+
109
+ def _webhook_response(request)
110
+ signature_header = request.env["HTTP_EMAILOCTOPUS_SIGNATURE"]
111
+ return Webhookdb::WebhookResponse.error("missing signature") if signature_header.nil?
112
+
113
+ request.body.rewind
114
+ data = request.body.read
115
+ verified = Webhookdb::EmailOctopus.verify_webhook(data, signature_header, self.service_integration.webhook_secret)
116
+ return Webhookdb::WebhookResponse.ok if verified
117
+ return Webhookdb::WebhookResponse.error("invalid signature")
118
+ end
119
+
120
+ def calculate_webhook_state_machine
121
+ if (step = self.calculate_dependency_state_machine_step(dependency_help: ""))
122
+ return step
123
+ end
124
+ step = Webhookdb::Replicator::StateMachineStep.new
125
+ unless self.service_integration.webhook_secret.present?
126
+ step.output = %(You are about to start replicating #{self.resource_name_plural} into WebhookDB.
127
+ We've made an endpoint available for #{self.resource_name_singular} webhooks:
128
+
129
+ #{self._webhook_endpoint}
130
+
131
+ - From your Email Octopus dashboard, go to Account Settings -> Integrations & API.
132
+ - Then click the 'Manage' button next to 'Webhooks'.
133
+ - Then under the "Endpoints" header, click "Add endpoint"
134
+ - In the "URL" field you can enter the URL above.
135
+ - Then check boxes for all events, because we want this webhook to listen for everything.
136
+ This includes:
137
+ - "Contact events to send" -> "Created", "Updated", and "Deleted"
138
+ - "Email events to send" -> Deleted" "Clicked", "Opened", "Bounced",
139
+ "Complained", and "Unsubscribed"
140
+ - You can keep the checkboxes under "Exclude contact events that occur" unchecked.
141
+
142
+ - Save the endpoint.
143
+
144
+ You'll be dropped back on the Webhooks page.
145
+ Click 'View Secret' next to the endpoint you added, and Copy it.
146
+ We'll use it for webhook verification.)
147
+ return step.secret_prompt("webhook secret").webhook_secret(self.service_integration)
148
+ end
149
+
150
+ step.output = %(Great! WebhookDB is now listening for #{self.resource_name_singular} webhooks.
151
+ #{self._query_help_output}
152
+ In order to backfill existing #{self.resource_name_plural}, run this from a shell:
153
+
154
+ #{self._backfill_command})
155
+ return step.completed
156
+ end
157
+
158
+ def calculate_backfill_state_machine
159
+ if (step = self.calculate_dependency_state_machine_step(dependency_help: ""))
160
+ return step
161
+ end
162
+ step = Webhookdb::Replicator::StateMachineStep.new
163
+ # We're using the API Key from the dependency, we don't need to ask for it here
164
+ step.output = %(Great! We are going to start replicating your #{self.resource_name_plural}.
165
+ #{self._query_help_output})
166
+ return step.completed
167
+ end
168
+
169
+ def on_dependency_webhook_upsert(_replicator, _payload, *)
170
+ return
171
+ end
172
+
173
+ CAMPAIGN_EVENT_TYPES = [
174
+ "bounced",
175
+ "clicked",
176
+ "complained",
177
+ "opened",
178
+ "unsubscribed",
179
+ ].freeze
180
+
181
+ def _backfillers
182
+ list_sint = self.service_integration.depends_on
183
+ api_key = list_sint.replicator.backfill_key!
184
+ campaign_sint = list_sint.replicator.find_dependent!("email_octopus_campaign_v1")
185
+ campaign_svc = campaign_sint.replicator
186
+ backfillers = campaign_svc.admin_dataset(timeout: :fast) do |campaign_ds|
187
+ campaign_ds.select(:email_octopus_id).flat_map do |campaign|
188
+ CAMPAIGN_EVENT_TYPES.map do |event_type|
189
+ EventBackfiller.new(
190
+ event_svc: self,
191
+ campaign_id: campaign[:email_octopus_id],
192
+ api_key:,
193
+ event_type:,
194
+ )
195
+ end
196
+ end
197
+ end
198
+ return backfillers
199
+ end
200
+
201
+ # Because "Event" is an abstraction we've created and does not actually exist in the Email Octopus API,
202
+ # it might seem strange that we are able to "backfill" the resources at all. However, the API has a number of
203
+ # endpoints that they call "campaign reports," which return lists of contacts that have engaged with a campaign
204
+ # in the specified way (e.g. bounced, opened, etc.) In this way, we can retrieve timestamped records of "events" that
205
+ # have already occurred. The only available campaign report endpoint that we don't hit is "sent", which notes the time
206
+ # at which each campaign was sent to each contact. This information doesn't come in through webhooks at all and the
207
+ # timestamps closely match the "sent_at" field in the campaign row, so we have opted not to track it through backfill.
208
+ class EventBackfiller < Webhookdb::Backfiller
209
+ include Webhookdb::Backfiller::Bulk
210
+
211
+ def initialize(event_svc:, campaign_id:, api_key:, event_type:)
212
+ @event_svc = event_svc
213
+ @campaign_id = campaign_id
214
+ @api_key = api_key
215
+ @event_type = event_type
216
+ super()
217
+ end
218
+
219
+ def upserting_replicator = @event_svc
220
+ def upsert_page_size = 500
221
+
222
+ def prepare_body(body)
223
+ body["campaign_id"] = @campaign_id
224
+ body["event_type"] = "contact.#{@event_type}"
225
+ end
226
+
227
+ def fetch_backfill_page(pagination_token, **_kwargs)
228
+ limit = Webhookdb::EmailOctopus.page_size
229
+ base_url = "https://emailoctopus.com"
230
+ # rubocop:disable Layout/LineLength
231
+ endpoint_path = pagination_token || "/api/1.6/campaigns/#{@campaign_id}/reports/#{@event_type}?api_key=#{@api_key}&limit=#{limit}"
232
+ # rubocop:enable Layout/LineLength
233
+ response = Webhookdb::Http.get(
234
+ base_url + endpoint_path,
235
+ logger: @event_svc.logger,
236
+ timeout: Webhookdb::EmailOctopus.http_timeout,
237
+ )
238
+ data = response.parsed_response
239
+ # if no data is returned from endpoint, the "paging" and "data" values are both empty arrays
240
+ next_page_link = data.fetch("paging").empty? ? nil : data.dig("paging", "next")
241
+ return data["data"], next_page_link
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/email_octopus"
4
+
5
+ class Webhookdb::Replicator::EmailOctopusListV1 < Webhookdb::Replicator::Base
6
+ include Appydays::Loggable
7
+
8
+ # @return [Webhookdb::Replicator::Descriptor]
9
+ def self.descriptor
10
+ return Webhookdb::Replicator::Descriptor.new(
11
+ name: "email_octopus_list_v1",
12
+ ctor: ->(sint) { Webhookdb::Replicator::EmailOctopusListV1.new(sint) },
13
+ feature_roles: [],
14
+ resource_name_singular: "Email Octopus List",
15
+ supports_backfill: true,
16
+ api_docs_url: "https://emailoctopus.com/api-documentation",
17
+ )
18
+ end
19
+
20
+ def _remote_key_column
21
+ return Webhookdb::Replicator::Column.new(:email_octopus_id, TEXT, data_key: "id")
22
+ end
23
+
24
+ def _denormalized_columns
25
+ return [
26
+ Webhookdb::Replicator::Column.new(:name, TEXT),
27
+ Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, index: true, converter: :time),
28
+ Webhookdb::Replicator::Column.new(:pending, INTEGER, data_key: ["counts", "pending"]),
29
+ Webhookdb::Replicator::Column.new(:subscribed, INTEGER, data_key: ["counts", "subscribed"]),
30
+ Webhookdb::Replicator::Column.new(:unsubscribed, INTEGER, data_key: ["counts", "unsubscribed"]),
31
+ Webhookdb::Replicator::Column.new(:row_updated_at, TIMESTAMP, defaulter: :now, optional: true),
32
+ ]
33
+ end
34
+
35
+ def _resource_and_event(request)
36
+ return request.body, nil
37
+ end
38
+
39
+ def _update_where_expr
40
+ return self.qualified_table_sequel_identifier[:data] !~ Sequel[:excluded][:data]
41
+ end
42
+
43
+ def _timestamp_column_name
44
+ return :row_updated_at
45
+ end
46
+
47
+ def _webhook_response(_request)
48
+ return Webhookdb::WebhookResponse.ok
49
+ end
50
+
51
+ def calculate_backfill_state_machine
52
+ step = Webhookdb::Replicator::StateMachineStep.new
53
+ unless self.service_integration.backfill_key.present?
54
+ step.output = %(In order to replicate #{self.resource_name_plural} into WebhookDB, we need an API Key.
55
+ From your Email Octopus dashboard, go to Account Settings -> Integrations & API.
56
+ Then, click through to the API menu, under the "Developer tools" header and create a key.
57
+
58
+ Copy the key.
59
+ )
60
+ return step.secret_prompt("API Key").backfill_key(self.service_integration)
61
+ end
62
+
63
+ unless (result = self.verify_backfill_credentials).verified
64
+ self.service_integration.replicator.clear_backfill_information
65
+ step.output = result.message
66
+ return step.secret_prompt("API Key").backfill_key(self.service_integration)
67
+ end
68
+
69
+ step.output = %(Great! We are going to start replicating your #{self.resource_name_plural}.
70
+ #{self._query_help_output}
71
+ )
72
+ return step.completed
73
+ end
74
+
75
+ def _verify_backfill_err_msg
76
+ return "It looks like that API key is invalid. Please reenter your API Key:"
77
+ end
78
+
79
+ def _fetch_backfill_page(pagination_token, **)
80
+ api_key = self.backfill_key!
81
+ limit = Webhookdb::EmailOctopus.page_size
82
+ base_url = "https://emailoctopus.com"
83
+ endpoint_path = pagination_token || "/api/1.6/lists?api_key=#{api_key}&limit=#{limit}"
84
+ response = Webhookdb::Http.get(
85
+ base_url + endpoint_path,
86
+ logger: self.logger,
87
+ timeout: Webhookdb::EmailOctopus.http_timeout,
88
+ )
89
+ data = response.parsed_response
90
+ next_page_link = data.dig("paging", "next")
91
+ return data["data"], next_page_link
92
+ end
93
+
94
+ def backfill_key!
95
+ bk = self.service_integration.backfill_key
96
+
97
+ return bk if bk.present?
98
+ raise Webhookdb::Replicator::CredentialsMissing,
99
+ "This integration requires that the #{self.descriptor.name} integration has a valid API Key"
100
+ end
101
+ end