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,506 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/postgres/model"
4
+ require "appydays/configurable"
5
+ require "stripe"
6
+ require "webhookdb/stripe"
7
+ require "webhookdb/jobs/replication_migration"
8
+
9
+ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
10
+ class SchemaMigrationError < StandardError; end
11
+
12
+ plugin :timestamps
13
+ plugin :soft_deletes
14
+ plugin :column_encryption do |enc|
15
+ enc.column :readonly_connection_url_raw
16
+ enc.column :admin_connection_url_raw
17
+ end
18
+
19
+ configurable(:organization) do
20
+ setting :max_query_rows, 1000
21
+ setting :database_migration_page_size, 1000
22
+ end
23
+
24
+ one_to_one :subscription, class: "Webhookdb::Subscription", key: :stripe_customer_id, primary_key: :stripe_customer_id
25
+ one_to_many :all_memberships, class: "Webhookdb::OrganizationMembership", order: :id
26
+ one_to_many :verified_memberships,
27
+ class: "Webhookdb::OrganizationMembership",
28
+ conditions: {verified: true},
29
+ adder: ->(om) { om.update(organization_id: id, verified: true) },
30
+ order: :id
31
+ one_to_many :invited_memberships,
32
+ class: "Webhookdb::OrganizationMembership",
33
+ conditions: {verified: false},
34
+ adder: ->(om) { om.update(organization_id: id, verified: false) },
35
+ order: :id
36
+ one_to_many :service_integrations, class: "Webhookdb::ServiceIntegration", order: :id
37
+ one_to_many :webhook_subscriptions, class: "Webhookdb::WebhookSubscription", order: :id
38
+ many_to_many :feature_roles, class: "Webhookdb::Role", join_table: :feature_roles_organizations, right_key: :role_id
39
+ one_to_many :all_webhook_subscriptions,
40
+ class: "Webhookdb::WebhookSubscription",
41
+ readonly: true,
42
+ dataset: (lambda do |r|
43
+ org_sints = Webhookdb::ServiceIntegration.where(organization_id: id)
44
+ r.associated_dataset.where(
45
+ Sequel[organization_id: id] |
46
+ Sequel[service_integration_id: org_sints.select(:id)],
47
+ )
48
+ end),
49
+ eager_loader: (lambda do |eo|
50
+ org_ids = eo[:id_map].keys
51
+ all_subs = Webhookdb::WebhookSubscription.
52
+ left_join(:service_integrations, {id: :service_integration_id}).
53
+ select(
54
+ Sequel[:webhook_subscriptions][Sequel.lit("*")],
55
+ Sequel[:service_integrations][:organization_id].as(:_sint_org_id),
56
+ ).where(
57
+ Sequel[Sequel[:webhook_subscriptions][:organization_id] => org_ids] |
58
+ Sequel[Sequel[:service_integrations][:organization_id] => org_ids],
59
+ ).all
60
+ all_subs_by_org = all_subs.group_by { |sub| sub[:organization_id] || sub[:_sint_org_id] }
61
+ eo[:rows].each do |org|
62
+ org.associations[:all_webhook_subscriptions] = all_subs_by_org.fetch(org.id, [])
63
+ end
64
+ end)
65
+ many_through_many :all_sync_targets,
66
+ [
67
+ [:service_integrations, :organization_id, :id],
68
+ ],
69
+ class: "Webhookdb::SyncTarget",
70
+ left_primary_key: :id,
71
+ right_primary_key: :service_integration_id,
72
+ read_only: true,
73
+ order: [:created_at, :id]
74
+ one_to_many :database_migrations, class: "Webhookdb::Organization::DatabaseMigration", order: Sequel.desc(:created_at)
75
+
76
+ dataset_module do
77
+ # Return orgs with the given id (if identifier is an integer), or key or name.
78
+ def with_identifier(identifier)
79
+ return self.where(id: identifier.to_i) if /^\d+$/.match?(identifier)
80
+ ds = self.where(Sequel[key: identifier] | Sequel[name: identifier])
81
+ return ds
82
+ end
83
+ end
84
+
85
+ def log_tags
86
+ return {
87
+ organization_id: self.id,
88
+ organization_key: self.key,
89
+ }
90
+ end
91
+
92
+ def before_validation
93
+ self.minimum_sync_seconds ||= Webhookdb::SyncTarget.default_min_period_seconds
94
+ self.key ||= Webhookdb.to_slug(self.name)
95
+ self.replication_schema ||= Webhookdb::Organization::DbBuilder.new(self).default_replication_schema
96
+ super
97
+ end
98
+
99
+ def self.create_if_unique(params)
100
+ self.db.transaction(savepoint: true) do
101
+ return Webhookdb::Organization.create(name: params[:name])
102
+ end
103
+ rescue Sequel::UniqueConstraintViolation
104
+ return nil
105
+ end
106
+
107
+ def admin_customers
108
+ return self.verified_memberships.filter(&:admin?).map(&:customer)
109
+ end
110
+
111
+ def alerting
112
+ return @alerting ||= Alerting.new(self)
113
+ end
114
+
115
+ def cli_editable_fields
116
+ return ["name", "billing_email"]
117
+ end
118
+
119
+ def readonly_connection(**kw, &)
120
+ return Webhookdb::ConnectionCache.borrow(self.readonly_connection_url_raw, **kw, &)
121
+ end
122
+
123
+ def admin_connection(**kw, &)
124
+ return Webhookdb::ConnectionCache.borrow(self.admin_connection_url_raw, **kw, &)
125
+ end
126
+
127
+ def execute_readonly_query(sql)
128
+ max_rows = self.max_query_rows || self.class.max_query_rows
129
+ return self.readonly_connection do |conn|
130
+ ds = conn.fetch(sql)
131
+ r = QueryResult.new
132
+ r.max_rows_reached = false
133
+ r.columns = ds.columns
134
+ r.rows = []
135
+ # Stream to avoid pulling in all rows of unlimited queries
136
+ ds.stream.each do |row|
137
+ if r.rows.length >= max_rows
138
+ r.max_rows_reached = true
139
+ break
140
+ end
141
+ r.rows << row.values
142
+ end
143
+ return r
144
+ end
145
+ end
146
+
147
+ class QueryResult
148
+ attr_accessor :rows, :columns, :max_rows_reached
149
+ end
150
+
151
+ # Return the readonly connection url, with the host set to public_host if set.
152
+ def readonly_connection_url
153
+ return self._public_host_connection_url(self.readonly_connection_url_raw)
154
+ end
155
+
156
+ # Return the admin connection url, with the host set to public_host if set.
157
+ def admin_connection_url
158
+ return self._public_host_connection_url(self.admin_connection_url_raw)
159
+ end
160
+
161
+ # Replace the host of the given URL with public_host if it is set,
162
+ # or return u if not.
163
+ #
164
+ # It's very important we store the 'raw' URL to the actual host,
165
+ # and the public host separately. This will allow us to, for example,
166
+ # modify the org to point to a new host,
167
+ # and then update the CNAME (finding it based on the public_host name)
168
+ # to point to that host.
169
+ protected def _public_host_connection_url(u)
170
+ return u if self.public_host.blank?
171
+ uri = URI(u)
172
+ uri.host = self.public_host
173
+ return uri.to_s
174
+ end
175
+
176
+ def dbname
177
+ raise Webhookdb::InvalidPrecondition, "no db has been created, call prepare_database_connections first" if
178
+ self.admin_connection_url.blank?
179
+ return URI(self.admin_connection_url).path.tr("/", "")
180
+ end
181
+
182
+ def admin_user
183
+ ur = URI(self.admin_connection_url)
184
+ return ur.user
185
+ end
186
+
187
+ def readonly_user
188
+ ur = URI(self.readonly_connection_url)
189
+ return ur.user
190
+ end
191
+
192
+ # In cases where the readonly and admin user are the same, we sometimes adapt queries
193
+ # to prevent revoking admin db priviliges.
194
+ def single_db_user?
195
+ return self.admin_user == self.readonly_user
196
+ end
197
+
198
+ def display_string
199
+ return "#{self.name} (#{self.key})"
200
+ end
201
+
202
+ def prepare_database_connections?
203
+ return self.prepare_database_connections(safe: true)
204
+ end
205
+
206
+ # Build the org-specific users, database, and set our connection URLs to it.
207
+ def prepare_database_connections(safe: false)
208
+ self.db.transaction do
209
+ self.lock!
210
+ if self.admin_connection_url.present?
211
+ return if safe
212
+ raise Webhookdb::InvalidPrecondition, "connections already set"
213
+ end
214
+ builder = Webhookdb::Organization::DbBuilder.new(self)
215
+ builder.prepare_database_connections
216
+ self.admin_connection_url_raw = builder.admin_url
217
+ self.readonly_connection_url_raw = builder.readonly_url
218
+ self.save_changes
219
+ end
220
+ end
221
+
222
+ # Create a CNAME in Cloudflare for the currently configured connection urls.
223
+ def create_public_host_cname
224
+ self.db.transaction do
225
+ self.lock!
226
+ # We must have a host to create a CNAME to.
227
+ raise Webhookdb::InvalidPrecondition, "connection urls must be set" if self.readonly_connection_url_raw.blank?
228
+ # Should only be used once when creating the org DBs.
229
+ raise Webhookdb::InvalidPrecondition, "public_host must not be set" if self.public_host.present?
230
+ # Use the raw URL, even though we know at this point
231
+ # public_host is empty so raw and public host urls are the same.
232
+ Webhookdb::Organization::DbBuilder.new(self).create_public_host_cname(self.readonly_connection_url_raw)
233
+ self.save_changes
234
+ end
235
+ end
236
+
237
+ # Delete the org-specific database and remove the org connection strings.
238
+ # Use this when an org is to be deleted (either for real, or in test teardown).
239
+ def remove_related_database
240
+ self.db.transaction do
241
+ self.lock!
242
+ Webhookdb::Organization::DbBuilder.new(self).remove_related_database
243
+ self.admin_connection_url_raw = ""
244
+ self.readonly_connection_url_raw = ""
245
+ self.save_changes
246
+ end
247
+ end
248
+
249
+ # As part of the release process, we enqueue a job that will migrate the replication schemas
250
+ # for all organizations. However this job must use the NEW code being released;
251
+ # it should not use the CURRENT code the workers may be using when this method is run
252
+ # during the release process.
253
+ #
254
+ # We can get around this by enqueing the jobs with the 'target' release creation date.
255
+ # Only jobs that execute with this release creation date will perform the migration;
256
+ # if the job is running using an older release creation date (ie still running old code),
257
+ # it will re-enqueue the migration to run in the future, using a worker that will eventually
258
+ # be using newer code.
259
+ #
260
+ # For example:
261
+ #
262
+ # - We have Release A, created at 0, currently running.
263
+ # - Release B, created at 1, runs this method.
264
+ # - The workers, using Release A code (with a release_created_at of 0),
265
+ # run the ReplicationMigration job.
266
+ # They see the target release_created_at of 1 is greater than/after the current release_created_at of 0,
267
+ # so re-enqueue the job.
268
+ # - Eventually the workers are using Release B code, which has a release_created_at of 1.
269
+ # This matches the target, so the job is run.
270
+ #
271
+ # For a more complex example, which involves releases created in quick succession
272
+ # (we need to be careful to avoid jobs that never run):
273
+ #
274
+ # - We have Release A, created at 0, currently running.
275
+ # - Release B, created at 1, runs this method.
276
+ # - Release C, created at 2, runs this method.
277
+ # - Workers are backed up, so nothing is processed until all workers are using Release C.
278
+ # - Workers using Release C code process two sets of jobs:
279
+ # - Jobs with a target release_created_at of 1
280
+ # - Jobs with a target release_created_at of 2
281
+ # - Jobs with a target of 2 run the actual migration, because the times match.
282
+ # - Jobs with a target of 1, see that the target is less than/before current release_created_at of 2.
283
+ # This indicates the migration is stale, and the job is discarded.
284
+ #
285
+ # NOTE: There will always be a race condition where we may process webhooks using the new code,
286
+ # before we've migrated the replication schemas into the new code. This will error during the upsert
287
+ # because the column doesn't yet exist. However these will be retried automatically,
288
+ # and quickly, so we don't worry about them yet.
289
+ def self.enqueue_migrate_all_replication_tables
290
+ Webhookdb::Organization.each do |org|
291
+ Webhookdb::Jobs::ReplicationMigration.perform_in(2, org.id, Webhookdb::RELEASE_CREATED_AT)
292
+ end
293
+ end
294
+
295
+ # Get all the table names and column names for all integrations in the org
296
+ # Find any of those table/column pairs that are not present in information_schema.columns
297
+ # Ensure all columns for those integrations/tables.
298
+ def migrate_replication_tables
299
+ tables = self.service_integrations.map(&:table_name)
300
+ sequences_in_app_db = self.db[Sequel[:information_schema][:sequences]].
301
+ grep(:sequence_name, "replicator_seq_org_#{self.id}_%").
302
+ select_map(:sequence_name).
303
+ to_set
304
+ cols_in_org_db = {}
305
+ indices_in_org_db = Set.new
306
+ self.admin_connection do |db|
307
+ cols_in_org_db = db[Sequel[:information_schema][:columns]].
308
+ where(table_schema: self.replication_schema, table_name: tables).
309
+ select(
310
+ :table_name,
311
+ Sequel.function(:array_agg, :column_name).cast("text[]").as(:columns),
312
+ ).
313
+ group_by(:table_name).
314
+ all.
315
+ to_h { |c| [c[:table_name], c[:columns]] }
316
+ indices_in_org_db = db[Sequel[:pg_indexes]].
317
+ where(schemaname: self.replication_schema, tablename: tables).
318
+ select_map(:indexname).
319
+ to_set
320
+ end
321
+
322
+ self.service_integrations.each do |sint|
323
+ svc = sint.replicator
324
+ existing_columns = cols_in_org_db.fetch(sint.table_name) { [] }
325
+ cols_for_sint = svc.storable_columns.map { |c| c.name.to_s }
326
+ all_sint_cols_exist = (cols_for_sint - existing_columns).empty?
327
+
328
+ all_indices_exist = svc.indices(svc.dbadapter_table).all? do |ind|
329
+ indices_in_org_db.include?(ind.name.to_s)
330
+ end
331
+
332
+ svc.ensure_all_columns unless all_sint_cols_exist && all_indices_exist
333
+ if svc.requires_sequence? && !sequences_in_app_db.include?(sint.sequence_name)
334
+ sint.ensure_sequence(skip_check: true)
335
+ end
336
+ end
337
+ end
338
+
339
+ # Modify the admin and readonly users to have new usernames and passwords.
340
+ def roll_database_credentials
341
+ self.db.transaction do
342
+ self.lock!
343
+ builder = Webhookdb::Organization::DbBuilder.new(self)
344
+ builder.roll_connection_credentials
345
+ self.admin_connection_url_raw = builder.admin_url
346
+ self.readonly_connection_url_raw = builder.readonly_url
347
+ self.save_changes
348
+ end
349
+ end
350
+
351
+ def migrate_replication_schema(schema)
352
+ unless Webhookdb::DBAdapter::VALID_IDENTIFIER.match?(schema)
353
+ msg = "Sorry, this is not a valid schema name. " + Webhookdb::DBAdapter::INVALID_IDENTIFIER_MESSAGE
354
+ raise SchemaMigrationError, msg
355
+ end
356
+ Webhookdb::Organization::DatabaseMigration.guard_ongoing!(self)
357
+ raise SchemaMigrationError, "destination and target schema are the same" if schema == self.replication_schema
358
+ builder = Webhookdb::Organization::DbBuilder.new(self)
359
+ sql = builder.migration_replication_schema_sql(self.replication_schema, schema)
360
+ self.admin_connection(transaction: true) do |db|
361
+ db << sql
362
+ end
363
+ self.update(replication_schema: schema)
364
+ end
365
+
366
+ def register_in_stripe
367
+ raise Webhookdb::InvalidPrecondition, "org already in Stripe" if self.stripe_customer_id.present?
368
+ stripe_customer = Stripe::Customer.create(
369
+ {
370
+ name: self.name,
371
+ email: self.billing_email,
372
+ metadata: {
373
+ org_id: self.id,
374
+ },
375
+ },
376
+ )
377
+ self.stripe_customer_id = stripe_customer.id
378
+ self.save_changes
379
+ return stripe_customer
380
+ end
381
+
382
+ def get_stripe_billing_portal_url
383
+ raise Webhookdb::InvalidPrecondition, "organization must be registered in Stripe" if self.stripe_customer_id.blank?
384
+ session = Stripe::BillingPortal::Session.create(
385
+ {
386
+ customer: self.stripe_customer_id,
387
+ return_url: Webhookdb.app_url + "/jump/portal-return",
388
+ },
389
+ )
390
+
391
+ return session.url
392
+ end
393
+
394
+ def get_stripe_checkout_url(price_id)
395
+ raise Webhookdb::InvalidPrecondition, "organization must be registered in Stripe" if self.stripe_customer_id.blank?
396
+ session = Stripe::Checkout::Session.create(
397
+ {
398
+ customer: self.stripe_customer_id,
399
+ cancel_url: Webhookdb.app_url + "/jump/checkout-cancel",
400
+ line_items: [{
401
+ price: price_id, quantity: 1,
402
+ }],
403
+ mode: "subscription",
404
+ payment_method_types: ["card"],
405
+ allow_promotion_codes: true,
406
+ success_url: Webhookdb.app_url + "/jump/checkout-success",
407
+ },
408
+ )
409
+
410
+ return session.url
411
+ end
412
+
413
+ #
414
+ # :section: Memberships
415
+ #
416
+
417
+ def add_membership(opts={})
418
+ if !opts.is_a?(Webhookdb::OrganizationMembership) && !opts.key?(:verified)
419
+ raise ArgumentError, "must pass :verified or a model into add_membership, it is ambiguous otherwise"
420
+ end
421
+ self.associations.delete(opts[:verified] ? :verified_memberships : :invited_memberships)
422
+ return self.add_all_membership(opts)
423
+ end
424
+
425
+ # SUBSCRIPTION PERMISSIONS
426
+
427
+ def active_subscription?
428
+ subscription = Webhookdb::Subscription[stripe_customer_id: self.stripe_customer_id]
429
+ # return false if no subscription
430
+ return false if subscription.nil?
431
+ # otherwise check stripe subscription string
432
+ return ["trialing", "active", "past due"].include? subscription.status
433
+ end
434
+
435
+ def can_add_new_integration?
436
+ # if the sint's organization has an active subscription, return true
437
+ return true if self.active_subscription?
438
+ # if there is no active subscription, check number of integrations against free tier max
439
+ limit = Webhookdb::Subscription.max_free_integrations
440
+ return Webhookdb::ServiceIntegration.where(organization: self).count < limit
441
+ end
442
+
443
+ def available_replicator_names
444
+ available = Webhookdb::Replicator.registry.values.filter do |desc|
445
+ # The org must have any of the flags required for the service. In other words,
446
+ # the intersection of desc[:feature_roles] & org.feature_roles must
447
+ # not be empty
448
+ no_restrictions = desc.feature_roles.empty?
449
+ next true if no_restrictions
450
+ org_has_access = (self.feature_roles.map(&:name) & desc.feature_roles).present?
451
+ org_has_access
452
+ end
453
+ return available.map(&:name)
454
+ end
455
+
456
+ #
457
+ # :section: Validations
458
+ #
459
+
460
+ def validate
461
+ super
462
+ validates_all_or_none(:admin_connection_url_raw, :readonly_connection_url_raw, predicate: :present?)
463
+ validates_format(/^\D/, :name, message: "can't begin with a digit")
464
+ validates_format(/^[a-z][a-z0-9_]*$/, :key, message: "is not valid as a CNAME")
465
+ validates_max_length 63, :key, message: "is not valid as a CNAME"
466
+ end
467
+
468
+ # @!attribute service_integrations
469
+ # @return [Array<Webhookdb::ServiceIntegration>]
470
+ end
471
+
472
+ require "webhookdb/organization/alerting"
473
+ require "webhookdb/organization/db_builder"
474
+
475
+ # Table: organizations
476
+ # ------------------------------------------------------------------------------------------------------------------------------------------------------------
477
+ # Columns:
478
+ # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
479
+ # created_at | timestamp with time zone | NOT NULL DEFAULT now()
480
+ # updated_at | timestamp with time zone |
481
+ # soft_deleted_at | timestamp with time zone |
482
+ # name | text | NOT NULL
483
+ # key | text |
484
+ # billing_email | text | NOT NULL DEFAULT ''::text
485
+ # stripe_customer_id | text | NOT NULL DEFAULT ''::text
486
+ # readonly_connection_url_raw | text |
487
+ # admin_connection_url_raw | text |
488
+ # public_host | text | NOT NULL DEFAULT ''::text
489
+ # cloudflare_dns_record_json | jsonb | NOT NULL DEFAULT '{}'::jsonb
490
+ # replication_schema | text | NOT NULL
491
+ # job_semaphore_size | integer | NOT NULL DEFAULT 10
492
+ # minimum_sync_seconds | integer | NOT NULL
493
+ # sync_target_timeout | integer | NOT NULL DEFAULT 30
494
+ # max_query_rows | integer |
495
+ # Indexes:
496
+ # organizations_pkey | PRIMARY KEY btree (id)
497
+ # organizations_key_key | UNIQUE btree (key)
498
+ # organizations_name_key | UNIQUE btree (name)
499
+ # Referenced By:
500
+ # feature_roles_organizations | feature_roles_organizations_organization_id_fkey | (organization_id) REFERENCES organizations(id)
501
+ # logged_webhooks | logged_webhooks_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
502
+ # organization_database_migrations | organization_database_migrations_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
503
+ # organization_memberships | organization_memberships_organization_id_fkey | (organization_id) REFERENCES organizations(id)
504
+ # service_integrations | service_integrations_organization_id_fkey | (organization_id) REFERENCES organizations(id)
505
+ # webhook_subscriptions | webhook_subscriptions_organization_id_fkey | (organization_id) REFERENCES organizations(id)
506
+ # ------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/postgres/model"
4
+
5
+ class Webhookdb::OrganizationMembership < Webhookdb::Postgres::Model(:organization_memberships)
6
+ VALID_ROLE_NAMES = ["admin", "member"].freeze
7
+
8
+ many_to_one :organization, class: "Webhookdb::Organization"
9
+ many_to_one :customer, class: "Webhookdb::Customer"
10
+ many_to_one :membership_role, class: "Webhookdb::Role"
11
+
12
+ def verified?
13
+ return self.verified
14
+ end
15
+
16
+ def default?
17
+ return self.is_default
18
+ end
19
+
20
+ def customer_email
21
+ return self.customer.email
22
+ end
23
+
24
+ def organization_name
25
+ return self.organization.name
26
+ end
27
+
28
+ def status
29
+ return "invited" unless self.verified
30
+ self.membership_role.name
31
+ end
32
+
33
+ def admin?
34
+ return self.membership_role.name == "admin"
35
+ end
36
+ end
37
+
38
+ # Table: organization_memberships
39
+ # ------------------------------------------------------------------------------------------------------------------------------
40
+ # Columns:
41
+ # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
42
+ # customer_id | integer | NOT NULL
43
+ # organization_id | integer | NOT NULL
44
+ # verified | boolean | NOT NULL
45
+ # invitation_code | text | NOT NULL DEFAULT ''::text
46
+ # membership_role_id | integer | NOT NULL
47
+ # is_default | boolean | NOT NULL DEFAULT false
48
+ # Indexes:
49
+ # organization_memberships_pkey | PRIMARY KEY btree (id)
50
+ # one_default_per_customer | UNIQUE btree (customer_id, organization_id) WHERE is_default IS TRUE
51
+ # Check constraints:
52
+ # default_is_verified | (is_default IS TRUE AND verified IS TRUE OR is_default IS FALSE)
53
+ # invited_has_code | (verified IS TRUE AND length(invitation_code) < 1 OR verified IS FALSE AND length(invitation_code) > 0)
54
+ # Foreign key constraints:
55
+ # organization_memberships_customer_id_fkey | (customer_id) REFERENCES customers(id)
56
+ # organization_memberships_membership_role_id_fkey | (membership_role_id) REFERENCES roles(id)
57
+ # organization_memberships_organization_id_fkey | (organization_id) REFERENCES organizations(id)
58
+ # ------------------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webhookdb::PhoneNumber
4
+ class US
5
+ REGEXP = /^1[0-9]{10}$/
6
+
7
+ def self.normalize(s)
8
+ norm = Phony.normalize(s, cc: "1")
9
+ norm = "1#{norm}" if norm.length == 10 && norm.first == "1"
10
+ return norm
11
+ end
12
+
13
+ def self.valid?(s)
14
+ return false if s.nil?
15
+ return self.valid_normalized?(self.normalize(s))
16
+ end
17
+
18
+ def self.valid_normalized?(s)
19
+ return REGEXP.match?(s)
20
+ end
21
+
22
+ def self.format(s)
23
+ raise ArgumentError, "#{s} must be a normalized to #{REGEXP}" unless self.valid_normalized?(s)
24
+ return "(#{s[1..3]}) #{s[4..6]}-#{s[7..]}"
25
+ end
26
+ end
27
+
28
+ # Given a string representing a phone number, returns that phone number in E.164 format (+1XXX5550100).
29
+ # Assumes all provided phone numbers are US numbers.
30
+ # Does not check for invalid area codes.
31
+ def self.format_e164(phone)
32
+ return nil if phone.blank?
33
+ return phone if /^\+1\d{10}$/.match?(phone)
34
+ phone = phone.gsub(/\D/, "")
35
+ return "+1" + phone if phone.size == 10
36
+ return "+" + phone if phone.size == 11
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appydays/configurable"
4
+
5
+ module Webhookdb::Plaid
6
+ include Appydays::Configurable
7
+
8
+ configurable(:plaid) do
9
+ setting :page_size, 100
10
+ setting :http_timeout, 30
11
+ end
12
+
13
+ # Manual backfilling is not supported on Plaid integrations.
14
+ # If a manual backfill is attempted, direct customer to this url.
15
+ DOCUMENTATION_URL = "https://docs.webhookdb.com/guides/plaid/"
16
+
17
+ def self.webhook_response(request, webhook_secret)
18
+ # Eventually we can figure out how to verify Plaid webhooks,
19
+ # but it's sort of crazy so ignore it for now.
20
+ return Webhookdb::WebhookResponse.ok(status: 202) if request.env["HTTP_PLAID_VERIFICATION"]
21
+ return Webhookdb::WebhookResponse.for_standard_secret(request, webhook_secret, ok_status: 200)
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webhookdb::Platform
4
+ PLATFORM_USER_AGENT_HTTP = "HTTP_WHDB_PLATFORM_USER_AGENT"
5
+ CLI_USER_AGENT_HTTP = "HTTP_WHDB_USER_AGENT"
6
+ USER_AGENT_HTTP = "HTTP_USER_AGENT"
7
+ HEADERS = [
8
+ PLATFORM_USER_AGENT_HTTP,
9
+ CLI_USER_AGENT_HTTP,
10
+ USER_AGENT_HTTP,
11
+ ].freeze
12
+
13
+ # Return the value of the platform UA header.
14
+ # For WASM, this is the browser's user agent.
15
+ # For a binary, this is empty.
16
+ def self.platform_user_agent(env)
17
+ return env[PLATFORM_USER_AGENT_HTTP] || ""
18
+ end
19
+
20
+ # Return the user agent in the env.
21
+ # This should be the CLI user agent,
22
+ # though it may also be a browser in WASM depending on the browser.
23
+ def self.user_agent(env)
24
+ values = HEADERS.map { |h| env[h] }
25
+ return values.find(&:present?) || ""
26
+ end
27
+ end