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,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