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,429 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appydays/configurable"
4
+ require "appydays/loggable"
5
+
6
+ require "webhookdb/organization"
7
+ require "webhookdb/cloudflare"
8
+
9
+ # When an org is created, it gets its own database containing the service integration tables
10
+ # exclusively for that org. This ensures orgs can be easily isolated from each other.
11
+ #
12
+ # Each org has two connections:
13
+ # - admin, which can modify tables. Used to create service integration tables.
14
+ # We generally want to avoid using this connection.
15
+ # Should never be exposed.
16
+ # - readonly, used for reading only the service integration tables
17
+ # (and not additional PG info tables).
18
+ # Can safely be exposed to members of the org.
19
+ class Webhookdb::Organization::DbBuilder
20
+ include Appydays::Configurable
21
+ include Appydays::Loggable
22
+ include Webhookdb::Dbutil
23
+ extend Webhookdb::MethodUtilities
24
+
25
+ class IsolatedOperationError < StandardError; end
26
+
27
+ DATABASE = "database"
28
+ SCHEMA = "schema"
29
+ USER = "user"
30
+ NONE = "none"
31
+
32
+ VALID_ISOLATION_MODES = [
33
+ "#{DATABASE}+#{USER}",
34
+ "#{DATABASE}+#{SCHEMA}+#{USER}",
35
+ SCHEMA,
36
+ "#{SCHEMA}+#{USER}",
37
+ NONE,
38
+ ].freeze
39
+
40
+ singleton_attr_accessor :available_server_urls
41
+
42
+ configurable(:db_builder) do
43
+ # Server urls are absolute urls that define servers which can be chosen for organization DBs.
44
+ # Space-separate multiple servers.
45
+ setting :server_urls, [], convert: ->(s) { s.split.map(&:strip) }
46
+ # Server env vars are the names of environment variables whose value are
47
+ # each a server which can be chosen for organization DBs.
48
+ # Allows you to use dynamically configured servers.
49
+ # Space-separate multiple env vars.
50
+ setting :server_env_vars, ["DATABASE_URL"], convert: ->(s) { s.split.map(&:strip) }
51
+ # Determines whether we allow orgs to handle their own migrations.
52
+ # Used for self-hosted databases.
53
+ setting :allow_public_migrations, false
54
+ # Create a CNAME record when building the database connection.
55
+ setting :create_cname_for_connection_urls, false
56
+ # The Cloudflare zone ID that DNS records will be created in.
57
+ # NOTE: It is required that the Cloudflare API token has access to this zone.
58
+ setting :cloudflare_dns_zone_id, "testdnszoneid"
59
+ # See README for more details.
60
+ setting :isolation_mode, "database+user"
61
+ # How many connections can each readonly user make?
62
+ setting :readonly_connection_limit, 50
63
+ # How long can the readonly user query before a timeout?
64
+ # Keep this low to avoid query pressure the database,
65
+ # especially when dealing with many tenants.
66
+ setting :readonly_connection_timeout, "15s"
67
+
68
+ after_configured do
69
+ unless VALID_ISOLATION_MODES.include?(self.isolation_mode)
70
+ msg = "Invalid DB_BUILDER_ISOLATION_MODE '#{self.isolation_mode}', " \
71
+ "valid modes are: #{VALID_ISOLATION_MODES.join(', ')}"
72
+ raise KeyError, msg
73
+ end
74
+ self.available_server_urls = self.server_urls.dup
75
+ self.available_server_urls.concat(self.server_env_vars.map { |e| ENV.fetch(e, nil) })
76
+ end
77
+ end
78
+
79
+ def self.isolate?(type)
80
+ return self.isolation_mode.include?(type)
81
+ end
82
+
83
+ attr_reader :admin_url, :readonly_url
84
+
85
+ def initialize(org)
86
+ @org = org
87
+ end
88
+
89
+ def default_replication_schema
90
+ raise Webhookdb::InvalidPrecondition, "Org must have a key to calculate the replication schema" if @org.key.blank?
91
+ return "public" unless self.class.isolate?(SCHEMA)
92
+ return "whdb_#{@org.key}"
93
+ end
94
+
95
+ def prepare_database_connections
96
+ # Grab a random server url. This will give us a 'superuser'-like url
97
+ # that can create roles and whatever else.
98
+ superuser_str = self._choose_superuser_url
99
+ case self.class.isolation_mode
100
+ when "database+user"
101
+ self._prepare_database_connections_database_user(superuser_str)
102
+ when "database+schema+user"
103
+ self._prepare_database_connections_database_schema_user(superuser_str)
104
+ when "schema"
105
+ self._prepare_database_connections_schema(superuser_str)
106
+ when "schema+user"
107
+ self._prepare_database_connections_schema_user(superuser_str)
108
+ when "none"
109
+ self._prepare_database_connections_none(superuser_str)
110
+ else
111
+ raise "Did not expect mode #{self.class.isolation_mode}"
112
+ end
113
+ return self
114
+ end
115
+
116
+ def _prepare_database_connections_database_user(superuser_url_str)
117
+ superuser_url = URI.parse(superuser_url_str)
118
+ # Use this superuser connection to create the admin role,
119
+ # which will be responsible for the database.
120
+ # While connected as the superuser, we can go ahead and create both roles.
121
+ admin_user = self.randident("ad")
122
+ admin_pwd = self.randident
123
+ ro_user = self.randident("ro")
124
+ ro_pwd = self.randident
125
+ dbname = self.randident("db")
126
+ # Do not log this
127
+ borrow_conn(superuser_url_str, loggers: []) do |conn|
128
+ conn << <<~SQL
129
+ CREATE ROLE #{admin_user} PASSWORD '#{admin_pwd}' NOSUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;
130
+ CREATE ROLE #{ro_user} PASSWORD '#{ro_pwd}' NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT LOGIN;
131
+ ALTER USER #{ro_user} WITH CONNECTION LIMIT #{self.class.readonly_connection_limit};
132
+ ALTER ROLE #{ro_user} SET statement_timeout = '#{self.class.readonly_connection_timeout}';
133
+ GRANT #{admin_user} TO CURRENT_USER;
134
+ SQL
135
+ # Cannot be in the same statement as above since that's one transaction.
136
+ conn << "CREATE DATABASE #{dbname} OWNER #{admin_user};"
137
+ conn << "REVOKE ALL PRIVILEGES ON DATABASE #{dbname} FROM public;"
138
+ end
139
+
140
+ # Now that we've created the admin role and have a database,
141
+ # we must disconnect and connect to the new database.
142
+ # This MUST be done as superuser; for some reason,
143
+ # the public schema (which we need to revoke on) belongs to the superuser,
144
+ # NOT the DB owner: https://pgsql-general.postgresql.narkive.com/X9VKOPIW
145
+ superuser_in_db_str = self._create_conn_url(superuser_url.user, superuser_url.password, superuser_url, dbname)
146
+ schema = self._org_schema
147
+ borrow_conn(superuser_in_db_str) do |conn|
148
+ conn << <<~SQL
149
+ -- Revoke all rights from readonly user, and public role, which all users have.
150
+ REVOKE ALL ON DATABASE #{dbname} FROM PUBLIC, #{ro_user};
151
+ REVOKE ALL ON SCHEMA public FROM PUBLIC, #{ro_user};
152
+ REVOKE ALL ON ALL TABLES IN SCHEMA public FROM PUBLIC, #{ro_user};
153
+ -- Create the schema if needed. In most cases this is 'public' so it isn't.
154
+ CREATE SCHEMA IF NOT EXISTS #{schema};
155
+ -- Allow the readonly user to select stuff
156
+ GRANT CONNECT ON DATABASE #{dbname} TO #{ro_user};
157
+ GRANT USAGE ON SCHEMA #{schema} TO #{ro_user};
158
+ GRANT SELECT ON ALL TABLES IN SCHEMA #{schema} TO #{ro_user};
159
+ -- Now that we have modified public/replication schema as superuser,
160
+ -- we can grant ownership to the admin user, so they can do modification in the future.
161
+ ALTER SCHEMA public OWNER TO #{admin_user};
162
+ ALTER SCHEMA #{schema} OWNER TO #{admin_user};
163
+ SQL
164
+ end
165
+ @admin_url = self._create_conn_url(admin_user, admin_pwd, superuser_url, dbname)
166
+ # We MUST modify the default privs AFTER changing ownership.
167
+ # Changing ownership seems to reset default piv grants (and it cannot be done after transferring ownership)
168
+ borrow_conn(@admin_url) do |conn|
169
+ conn << "ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema} GRANT SELECT ON TABLES TO #{ro_user};"
170
+ end
171
+ @readonly_url = self._create_conn_url(ro_user, ro_pwd, superuser_url, dbname)
172
+ end
173
+
174
+ def _prepare_database_connections_database_schema_user(superuser_url_str)
175
+ self._prepare_database_connections_database_user(superuser_url_str)
176
+ # Revoke everything on public schema, so our readonly user cannot access it.
177
+ borrow_conn(@admin_url) do |conn|
178
+ conn << "REVOKE ALL ON SCHEMA public FROM public"
179
+ end
180
+ end
181
+
182
+ def _prepare_database_connections_schema(superuser_url_str)
183
+ borrow_conn(superuser_url_str) do |conn|
184
+ conn << "CREATE SCHEMA IF NOT EXISTS #{self._org_schema};"
185
+ end
186
+ @admin_url = superuser_url_str
187
+ @readonly_url = superuser_url_str
188
+ end
189
+
190
+ def _prepare_database_connections_schema_user(superuser_url_str)
191
+ ro_user = self.randident("ro")
192
+ ro_pwd = self.randident
193
+ schema = self._org_schema
194
+ borrow_conn(superuser_url_str) do |conn|
195
+ conn << <<~SQL
196
+ -- Create readonly role and make sure it cannot access public stuff
197
+ CREATE ROLE #{ro_user} PASSWORD '#{ro_pwd}' NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT LOGIN;
198
+ ALTER USER #{ro_user} WITH CONNECTION LIMIT #{self.class.readonly_connection_limit};
199
+ ALTER ROLE #{ro_user} SET statement_timeout = '#{self.class.readonly_connection_timeout}';
200
+ REVOKE ALL ON SCHEMA public FROM #{ro_user};
201
+ REVOKE CREATE ON SCHEMA public FROM #{ro_user};
202
+ REVOKE ALL ON ALL TABLES IN SCHEMA public FROM #{ro_user};
203
+ -- Create the schema and ensure readonly user can access it
204
+ -- Also remove public role access so other readonly users cannot access it.
205
+ CREATE SCHEMA IF NOT EXISTS #{schema};
206
+ REVOKE ALL ON SCHEMA #{schema} FROM PUBLIC, #{ro_user};
207
+ REVOKE ALL ON ALL TABLES IN SCHEMA #{schema} FROM PUBLIC, #{ro_user};
208
+ GRANT USAGE ON SCHEMA #{schema} TO #{ro_user};
209
+ GRANT SELECT ON ALL TABLES IN SCHEMA #{schema} TO #{ro_user};
210
+ ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema} GRANT SELECT ON TABLES TO #{ro_user};
211
+ SQL
212
+ end
213
+ @admin_url = superuser_url_str
214
+ @readonly_url = self._replace_url_auth(superuser_url_str, ro_user, ro_pwd)
215
+ end
216
+
217
+ def _prepare_database_connections_none(superuser_url_str)
218
+ @admin_url = superuser_url_str
219
+ @readonly_url = superuser_url_str
220
+ end
221
+
222
+ # Return the superuser url for the org to use when creating its DB connections.
223
+ # Right now this is just choosing a random server url,
224
+ # but could be more complex, like choosing the server with the lowest utilization.
225
+ protected def _choose_superuser_url
226
+ superuser_str = self.class.available_server_urls.sample
227
+ return superuser_str
228
+ end
229
+
230
+ protected def _org_schema
231
+ return Webhookdb::DBAdapter::PG.new.escape_identifier(@org.replication_schema)
232
+ end
233
+
234
+ # prefix with <id>a to avoid ever conflicting database names
235
+ def randident(prefix="")
236
+ return "a#{prefix}#{@org.id}a#{SecureRandom.hex(8)}"
237
+ end
238
+
239
+ def _create_conn_url(username, password, uri, dbname)
240
+ return "postgres://#{username}:#{password}@#{uri.host}:#{uri.port}/#{dbname}"
241
+ end
242
+
243
+ def _replace_url_auth(url, user, pass)
244
+ uri = URI(url)
245
+ uri.user = user
246
+ uri.password = pass
247
+ return uri.to_s
248
+ end
249
+
250
+ # Create the CNAME record specific to this org,
251
+ # so that the DB it is hosted on is reachable via a nice URL,
252
+ # rather than connecting directly to the host.
253
+ #
254
+ # If create_cnames is false,
255
+ # this method no-ops, so we don't spam cloudflare during integration tests,
256
+ # or need to httpmock everything during unit tests.
257
+ #
258
+ # Otherwise, create the CNAME like "myorg.db node.rds.amazonaws.com",
259
+ # and set org.public_host to "myorg.db.webhookdb.dev". The "webhookdb.dev" value
260
+ # comes from the Cloudflare DNS response, and corresponds to the zone
261
+ # that `cloudflare_dns_zone_id` identifies.
262
+ def create_public_host_cname(conn_url)
263
+ return nil unless self.class.create_cname_for_connection_urls
264
+ db_host = URI.parse(conn_url).host
265
+ cname = Webhookdb::Cloudflare.create_zone_dns_record(
266
+ type: "CNAME",
267
+ zone_id: self.class.cloudflare_dns_zone_id,
268
+ name: "#{@org.key}.db",
269
+ content: db_host,
270
+ )
271
+ @org.public_host = cname["result"]["name"]
272
+ @org.cloudflare_dns_record_json = cname
273
+ return self
274
+ end
275
+
276
+ # To remove related databases and users, we must
277
+ # 1) find the server hosting the database, which will itself contain the admin creds
278
+ # suitable for having created it in the first place.
279
+ # 2) delete the database, using info extracted from the admin connection (run from the server conn)
280
+ # 3) delete each user (run from the server conn)
281
+ # We need these workarounds because we cannot drop the admin database while we're connected to it
282
+ # (and we probably don't want the admin role trying to delete itself).
283
+ def remove_related_database
284
+ return if @org.admin_connection_url_raw.blank?
285
+ superuser_str = self._find_superuser_url_str
286
+ # Cannot use conn cache since we may be removing ourselves
287
+ borrow_conn(superuser_str) do |conn|
288
+ case self.class.isolation_mode
289
+ when "database+user", "database+schema+user"
290
+ Webhookdb::ConnectionCache.disconnect(@org.admin_connection_url_raw)
291
+ Webhookdb::ConnectionCache.disconnect(@org.readonly_connection_url_raw)
292
+ conn << "DROP DATABASE #{@org.dbname};"
293
+ conn << "DROP USER #{@org.readonly_user};" unless @org.single_db_user?
294
+ conn << "DROP USER #{@org.admin_user};"
295
+ when "schema+user"
296
+ Webhookdb::ConnectionCache.disconnect(@org.readonly_connection_url_raw)
297
+ conn << <<~SQL
298
+ DROP SCHEMA IF EXISTS #{self._org_schema} CASCADE;
299
+ DROP OWNED BY #{@org.readonly_user};
300
+ DROP USER #{@org.readonly_user};
301
+ SQL
302
+ when "schema"
303
+ conn << "DROP SCHEMA IF EXISTS #{self._org_schema} CASCADE"
304
+ when "none"
305
+ nil
306
+ else
307
+ raise "not supported yet"
308
+ end
309
+ end
310
+ end
311
+
312
+ protected def _find_superuser_url_str
313
+ admin_url = URI.parse(@org.admin_connection_url_raw)
314
+ superuser_str = self.class.available_server_urls.find do |sstr|
315
+ surl = URI.parse(sstr)
316
+ surl.host == admin_url.host && surl.port == admin_url.port
317
+ end
318
+ if superuser_str.blank?
319
+ msg = "Could not find a matching server url for #{admin_url} in #{self.class.available_server_urls}"
320
+ raise msg
321
+ end
322
+ return superuser_str
323
+ end
324
+
325
+ def roll_connection_credentials
326
+ raise IsolatedOperationError, "cannot roll credentials without a user isolation mode" unless
327
+ self.class.isolate?(USER)
328
+ superuser_uri = URI(self._find_superuser_url_str)
329
+ orig_readonly_user = URI(@org.readonly_connection_url_raw).user
330
+ ro_user = self.randident("ro")
331
+ ro_pwd = self.randident
332
+ @readonly_url = self._create_conn_url(ro_user, ro_pwd, superuser_uri, @org.dbname)
333
+ lines = [
334
+ "ALTER ROLE #{orig_readonly_user} RENAME TO #{ro_user};",
335
+ "ALTER ROLE #{ro_user} WITH PASSWORD '#{ro_pwd}';",
336
+ ]
337
+ if self.class.isolate?(DATABASE)
338
+ # Roll admin credentials for a separate database.
339
+ # For schema isolation, we assume admin is the superuser so cannot roll creds.
340
+ orig_admin_user = URI(@org.admin_connection_url_raw).user
341
+ admin_user = self.randident("ad")
342
+ admin_pwd = self.randident
343
+ lines.push(
344
+ "ALTER ROLE #{orig_admin_user} RENAME TO #{admin_user};",
345
+ "ALTER ROLE #{admin_user} WITH PASSWORD '#{admin_pwd}';",
346
+ )
347
+ @admin_url = self._create_conn_url(admin_user, admin_pwd, superuser_uri, @org.dbname)
348
+ else
349
+ @admin_url = @org.admin_connection_url_raw
350
+ end
351
+ # New conn so we don't log it
352
+ borrow_conn(superuser_uri.to_s, loggers: []) do |conn|
353
+ conn << lines.join("\n")
354
+ end
355
+ end
356
+
357
+ def generate_fdw_payload(
358
+ remote_server_name:,
359
+ fetch_size:,
360
+ local_schema:,
361
+ view_schema:
362
+ )
363
+ raise ArgumentError, "no arg can be blank" if
364
+ [remote_server_name, fetch_size, local_schema, view_schema].any?(&:blank?)
365
+ conn = URI(@org.readonly_connection_url)
366
+ fdw_sql = <<~FDW_SERVER
367
+ CREATE EXTENSION IF NOT EXISTS postgres_fdw;
368
+ DROP SERVER IF EXISTS #{remote_server_name} CASCADE;
369
+ CREATE SERVER #{remote_server_name}
370
+ FOREIGN DATA WRAPPER postgres_fdw
371
+ OPTIONS (host '#{conn.host}', port '#{conn.port}', dbname '#{conn.path[1..]}', fetch_size '#{fetch_size}');
372
+
373
+ CREATE USER MAPPING FOR CURRENT_USER
374
+ SERVER #{remote_server_name}
375
+ OPTIONS (user '#{conn.user}', password '#{conn.password}');
376
+
377
+ CREATE SCHEMA IF NOT EXISTS #{local_schema};
378
+ IMPORT FOREIGN SCHEMA #{self._org_schema}
379
+ FROM SERVER #{remote_server_name}
380
+ INTO #{local_schema};
381
+
382
+ CREATE SCHEMA IF NOT EXISTS #{view_schema};
383
+ FDW_SERVER
384
+
385
+ views_for_integrations = @org.service_integrations.to_h do |sint|
386
+ cmd = "CREATE MATERIALIZED VIEW IF NOT EXISTS #{view_schema}.#{sint.service_name} " \
387
+ "AS SELECT * FROM #{local_schema}.#{sint.table_name};"
388
+ [sint.opaque_id, cmd]
389
+ end
390
+ views_sql = views_for_integrations.values.sort.join("\n")
391
+
392
+ result = {
393
+ fdw_sql:,
394
+ views_sql:,
395
+ compound_sql: "#{fdw_sql}\n\n#{views_sql}",
396
+ views: views_for_integrations,
397
+ }
398
+ return result
399
+ end
400
+
401
+ def migration_replication_schema_sql(old_schema, new_schema)
402
+ can_migrate_to_public = self.class.isolate?(DATABASE)
403
+ if new_schema == "public" && !can_migrate_to_public
404
+ raise IsolatedOperationError,
405
+ "cannot migrate to public schema when using '#{self.class.isolation_mode}' isolation"
406
+ end
407
+ ad = Webhookdb::DBAdapter::PG.new
408
+ qold_schema = ad.escape_identifier(old_schema)
409
+ qnew_schema = ad.escape_identifier(new_schema)
410
+ lines = []
411
+ # lines << "ALTER SCHEMA #{qold_schema} RENAME TO #{qnew_schema};"
412
+ # lines << "CREATE SCHEMA IF NOT EXISTS public;"
413
+ lines << "CREATE SCHEMA IF NOT EXISTS #{qnew_schema};"
414
+ @org.service_integrations.each do |sint|
415
+ lines << ("ALTER TABLE IF EXISTS %s.%s SET SCHEMA %s;" %
416
+ [qold_schema, ad.escape_identifier(sint.table_name), qnew_schema])
417
+ end
418
+ if self.class.isolate?(USER)
419
+ ro_user = @org.readonly_user
420
+ lines << "GRANT USAGE ON SCHEMA #{qnew_schema} TO #{ro_user};"
421
+ lines << "GRANT SELECT ON ALL TABLES IN SCHEMA #{qnew_schema} TO #{ro_user};"
422
+ lines << "REVOKE ALL ON SCHEMA #{qold_schema} FROM #{ro_user};" unless @org.single_db_user?
423
+ lines << "REVOKE ALL ON ALL TABLES IN SCHEMA #{qold_schema} FROM #{ro_user};" unless @org.single_db_user?
424
+ lines << "ALTER DEFAULT PRIVILEGES IN SCHEMA #{qnew_schema} GRANT SELECT ON TABLES TO #{ro_user};"
425
+ end
426
+ # lines << "DROP SCHEMA #{qold_schema} CASCADE;"
427
+ return lines.join("\n")
428
+ end
429
+ end