webhookdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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