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,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/db_adapter"
4
+
5
+ class Webhookdb::Replicator::Column
6
+ include Webhookdb::DBAdapter::ColumnTypes
7
+
8
+ class IsomorphicProc < Webhookdb::TypedStruct
9
+ attr_reader :ruby, :sql
10
+ end
11
+
12
+ NOT_IMPLEMENTED = ->(*) { raise NotImplementedError }
13
+
14
+ # Convert a Unix timestamp (fractional seconds) to a Datetime.
15
+ CONV_UNIX_TS = IsomorphicProc.new(
16
+ ruby: lambda do |i, **_|
17
+ return Time.at(i)
18
+ rescue TypeError
19
+ return nil
20
+ end,
21
+ sql: lambda do |i|
22
+ # We do not have the 'rescue TypeError' behavior here yet.
23
+ # It is a beast to add in because we can't easily check if something is convertable,
24
+ # nor can we easily exception handle without creating a stored function.
25
+ Sequel.function(:to_timestamp, Sequel.cast(i, :double))
26
+ end,
27
+ )
28
+ # Parse a value as an integer. Remove surrounding quotes.
29
+ CONV_TO_I = IsomorphicProc.new(
30
+ ruby: ->(i, **_) { i.nil? ? nil : i.delete_prefix('"').delete_suffix('"').to_i },
31
+ sql: ->(i) { Sequel.cast(i, :integer) },
32
+ )
33
+ # Given a Datetime, convert it to UTC and truncate to a Date.
34
+ CONV_TO_UTC_DATE = IsomorphicProc.new(
35
+ ruby: ->(t, **_) { t&.in_time_zone("UTC")&.to_date },
36
+ sql: lambda do |i|
37
+ ts = Sequel.cast(i, :timestamptz)
38
+ in_utc = Sequel.function(:timezone, "UTC", ts)
39
+ Sequel.cast(in_utc, :date)
40
+ end,
41
+ )
42
+ # Parse a value using Time.parse.
43
+ CONV_PARSE_TIME = IsomorphicProc.new(
44
+ ruby: ->(value, **_) { value.nil? ? nil : Time.parse(value) },
45
+ sql: ->(i) { Sequel.cast(i, :timestamptz) },
46
+ )
47
+
48
+ # Parse a value using Date.parse.
49
+ CONV_PARSE_DATE = IsomorphicProc.new(
50
+ ruby: ->(value, **_) { value.nil? ? nil : Date.parse(value) },
51
+ sql: ->(i) { Sequel.cast(i, :date) },
52
+ )
53
+
54
+ CONV_COMMA_SEP = IsomorphicProc.new(
55
+ ruby: ->(value, **_) { value.nil? ? [] : value.split(",").map(&:strip) },
56
+ sql: lambda do |_e, json_path:, source_col:|
57
+ e = source_col.get_text(json_path)
58
+ parts = Sequel.function(:string_to_array, e, ",")
59
+ parts = Sequel.function(:unnest, parts)
60
+ sel = Webhookdb::Dbutil::MOCK_CONN.
61
+ from(parts.as(:parts)).
62
+ select(Sequel.function(:trim, :parts))
63
+ f = Sequel.function(:array, sel)
64
+ return f
65
+ end,
66
+ )
67
+
68
+ # Return a converter that parses a value using the given regex,
69
+ # and returns the capture group at index.
70
+ # The 'coerce' function can be applied to, for example,
71
+ # capture a number from a request path and store it as an integer.
72
+ #
73
+ # @param pattern [String]
74
+ # @param dbtype [Symbol] The DB type to use, like INTEGER or BIGINT.
75
+ #
76
+ # @note Only the first capture group can be extracted at this time.
77
+ def self.converter_from_regex(pattern, dbtype: nil)
78
+ re = self._assert_regex_converter_type(pattern)
79
+ case dbtype
80
+ when INTEGER
81
+ rcoerce = :to_i
82
+ pgcast = :integer
83
+ when BIGINT
84
+ rcoerce = :to_i
85
+ pgcast = :bigint
86
+ when nil
87
+ rcoerce = nil
88
+ pgcast = nil
89
+ else
90
+ raise NotImplementedError, "unhandled converter_from_regex dbtype: #{dbtype}"
91
+ end
92
+ return IsomorphicProc.new(
93
+ ruby: lambda do |value, **_|
94
+ matched = value&.match(re) do |md|
95
+ md.captures ? md.captures[0] : nil
96
+ end
97
+ (matched = matched.send(rcoerce)) if !matched.nil? && rcoerce
98
+ matched
99
+ end,
100
+ sql: lambda do |e|
101
+ f = Sequel.function(:substring, e.cast(:text), pattern)
102
+ f = f.cast(pgcast) if pgcast
103
+ f
104
+ end,
105
+ )
106
+ end
107
+
108
+ # Extract a number from a string using the given regexp.
109
+ # If nothing can be extracted, get the next value from the sequence.
110
+ #
111
+ # Note this requires `requires_sequence=true` on the replicator.
112
+ #
113
+ # Used primarily where the ID is sent by an API only in the request URL (not a key in the body),
114
+ # and the URL will not include an ID when it's being sent for the first time.
115
+ # We see this in channel manager APIs primarily, that replicate their data to 3rd parties.
116
+ #
117
+ # @note This converter does not work for backfilling/UPDATE of existing columns.
118
+ # It is generally only of use for unique ids.
119
+ def self.converter_int_or_sequence_from_regex(re, dbtype: BIGINT)
120
+ return Webhookdb::Replicator::Column::IsomorphicProc.new(
121
+ ruby: lambda do |value, service_integration:, **kw|
122
+ url_id = Webhookdb::Replicator::Column.converter_from_regex(re, dbtype:).
123
+ ruby.call(value, service_integration:, **kw)
124
+ url_id || service_integration.sequence_nextval
125
+ end,
126
+ sql: NOT_IMPLEMENTED,
127
+ )
128
+ end
129
+
130
+ # Parse the value in the column using the given strptime string.
131
+ #
132
+ # To provide an `sql` proc, provide the sqlformat string, which is used in TO_TIMESTAMP(col, sqlformat).
133
+ # Note that TO_TIMESTAMP does not support timezone offsets,
134
+ # so the time will always be in UTC.
135
+ #
136
+ # Future note: We may want to derive sqlformat from format,
137
+ # and handle timezone offsets in the timestamp strings.
138
+ def self.converter_strptime(format, sqlformat=nil, cls: Time)
139
+ return Webhookdb::Replicator::Column::IsomorphicProc.new(
140
+ ruby: lambda do |value, **|
141
+ value.nil? ? nil : cls.strptime(value, format)
142
+ end,
143
+ sql: lambda do |e|
144
+ raise NotImplementedError if sqlformat.nil?
145
+ f = Sequel.function(:to_timestamp, e, sqlformat)
146
+ f = f.cast(:date) if cls == Date
147
+ f
148
+ end,
149
+ )
150
+ end
151
+
152
+ def self.converter_gsub(pattern, replacement)
153
+ re = self._assert_regex_converter_type(pattern)
154
+ return Webhookdb::Replicator::Column::IsomorphicProc.new(
155
+ ruby: lambda do |value, **|
156
+ value&.gsub(re, replacement)
157
+ end,
158
+ sql: lambda do |e|
159
+ Sequel.function(:regexp_replace, e, pattern, replacement, "g")
160
+ end,
161
+ )
162
+ end
163
+
164
+ def self.converter_array_element(index:, sep:, cls: DECIMAL)
165
+ case cls
166
+ when DECIMAL
167
+ to_ruby = ->(v) { BigDecimal(v) }
168
+ to_sql = ->(e) { Sequel.cast(e, :decimal) }
169
+ else
170
+ raise ArgumentError, "Unsupported cls" unless valid_cls.include?(cls)
171
+ end
172
+
173
+ return IsomorphicProc.new(
174
+ ruby: lambda do |value, **|
175
+ break nil if value.nil?
176
+ parts = value.split(sep)
177
+ break nil if index >= parts.size
178
+ to_ruby.call(parts[index])
179
+ end,
180
+ sql: lambda do |expr|
181
+ # The expression may be a JSONB field, of the type jsonb (accessed with -> rather than ->>).
182
+ # Make sure it's text. The CAST will turn 'a' into '"a"' though, so we also need to trim quotes.
183
+ str_expr = Sequel.cast(expr, :text)
184
+ str_expr = Sequel.function(:btrim, str_expr, '"')
185
+ field_expr = Sequel.function(:split_part, str_expr, sep, index + 1)
186
+ # If the field is invalid, we get ''. Use nil in this case.
187
+ case_expr = Sequel.case({Sequel[field_expr => ""] => nil}, field_expr)
188
+ to_sql.call(case_expr)
189
+ end,
190
+ )
191
+ end
192
+
193
+ def self.converter_array_pluck(key, coltype)
194
+ pgtype = Webhookdb::DBAdapter::PG::COLTYPE_MAP.fetch(coltype)
195
+ return IsomorphicProc.new(
196
+ ruby: lambda do |value, **|
197
+ break nil if value.nil?
198
+ break nil unless value.respond_to?(:to_ary)
199
+ value.map { |v| v[key] }
200
+ end,
201
+ sql: lambda do |expr|
202
+ expr = Sequel.lit("'#{JSON.generate(expr)}'::jsonb") if expr.is_a?(Hash) || expr.is_a?(Array)
203
+ Webhookdb::Dbutil::MOCK_CONN.
204
+ from(Sequel.function(:jsonb_to_recordset, expr).as(Sequel.lit("x(#{key} #{pgtype})"))).
205
+ select(Sequel.function(:array_agg, Sequel.lit(key)))
206
+ end,
207
+ )
208
+ end
209
+
210
+ DAYS_OF_WEEK = [
211
+ "SUNDAY",
212
+ "MONDAY",
213
+ "TUESDAY",
214
+ "WEDNESDAY",
215
+ "THURSDAY",
216
+ "FRIDAY",
217
+ "SATURDAY",
218
+ ].freeze
219
+
220
+ # Convert a value or array by looking up its value in a map.
221
+ # @param array [Boolean] If true, the empty value is an array. If false, nil.
222
+ # @param map [Hash]
223
+ def self.converter_map_lookup(array:, map:)
224
+ empty = array ? Sequel.pg_array([]) : nil
225
+ return IsomorphicProc.new(
226
+ ruby: lambda do |value, **|
227
+ break empty if value.nil?
228
+ is_ary = value.respond_to?(:to_ary)
229
+ r = (is_ary ? value : [value]).map do |v|
230
+ if (mapval = map[v])
231
+ mapval
232
+ else
233
+ v
234
+ end
235
+ end
236
+ break is_ary ? r : r[0]
237
+ end,
238
+ sql: NOT_IMPLEMENTED,
239
+ )
240
+ end
241
+
242
+ KNOWN_CONVERTERS = {
243
+ date: CONV_PARSE_DATE,
244
+ time: CONV_PARSE_TIME,
245
+ to_i: CONV_TO_I,
246
+ tsat: CONV_UNIX_TS,
247
+ }.freeze
248
+
249
+ DEFAULTER_NOW = IsomorphicProc.new(ruby: ->(*) { Time.now }, sql: ->(*) { Sequel.function(:now) })
250
+ DEFAULTER_FALSE = IsomorphicProc.new(ruby: ->(*) { false }, sql: ->(*) { false })
251
+ DEFAULTER_FROM_INTEGRATION_SEQUENCE = IsomorphicProc.new(
252
+ ruby: ->(service_integration:, **_) { service_integration.sequence_nextval },
253
+ sql: ->(service_integration:) { Sequel.function(:nextval, service_integration.sequence_name) },
254
+ )
255
+
256
+ def self.defaulter_from_resource_field(key)
257
+ return Webhookdb::Replicator::Column::IsomorphicProc.new(
258
+ ruby: ->(resource:, **_) { resource.fetch(key.to_s) },
259
+ sql: ->(*) { key.to_sym },
260
+ )
261
+ end
262
+ KNOWN_DEFAULTERS = {now: DEFAULTER_NOW, tofalse: DEFAULTER_FALSE}.freeze
263
+
264
+ # Use in data_key when a value is an array, and you want to map a value from the array.
265
+ EACH_ITEM = :_each_item
266
+
267
+ # @return [Symbol]
268
+ attr_reader :name
269
+ # @return [Symbol]
270
+ attr_reader :type
271
+ # @return [Boolean]
272
+ attr_reader :index
273
+ alias index? index
274
+
275
+ # True if thie index should be a partial index, using WHERE (col IS NOT NULL).
276
+ # The #index attribute must be true.
277
+ # @return [Boolean]
278
+ attr_reader :index_not_null
279
+
280
+ # While :name, :type, and :index are pretty standard attributes for defining a database column,
281
+ # the rest of these attributes are specialized to WebhookDB and deal with how we are finding
282
+ # and interpreting the values given to us by external services.
283
+
284
+ # `data_key` is the key we look for in the resource object. If this value is an array we will
285
+ # `_dig` through the object using each key successively. `data_key` defaults to the string version
286
+ # of whatever name you provide for the column.
287
+ # @return [String,Array<String>]
288
+ attr_reader :data_key
289
+
290
+ # `event_key` is the key we look for in the event object. This defaults to nil, but note that if
291
+ # both an event object and event key are provided, we will always grab the value from the event object
292
+ # instead of from the resource object using the `data_key`. If this value is an array we will
293
+ # `_dig` through the object using each key successively, same as with `data_key`.
294
+ # @return [String,Array<String>]
295
+ attr_reader :event_key
296
+
297
+ # If `from_enrichment` is set then we use the `data_key` value to find the desired value in the
298
+ # enrichment object. In this case, if the enrichment object is nil you will get an error.
299
+ # @return [Boolean]
300
+ attr_reader :from_enrichment
301
+
302
+ # If `optional` is true then the column will be populated with a nil value instead of throwing an error
303
+ # if the desired value is not present in the object you're `_dig`ging into, which could be any of the
304
+ # three (resource, event, and enrichment) according to the way the rest of the attributes are configured.
305
+ # Note that for nested values, `_dig` will return nil if *any* of the keys in the provided array are
306
+ # missing from the object.
307
+ # @return [Boolean]
308
+ attr_reader :optional
309
+
310
+ # Sometimes we need to do some processing on the value provided by the external service so that the we
311
+ # get the data we want in the format we want. A common example is parsing various DateTime formats into our
312
+ # desired timestamp format. In these cases, we use a `converter`, which is an `IsomorphicProc` where both
313
+ # procs take the value retrieved from the external service and the resource object and return a value
314
+ # consistent with the column's type attribute.
315
+ #
316
+ # @return [IsomorphicProc]
317
+ # The 'ruby' proc accepts (value, resource:, event:, enrichment:, service_integration:) and returns a value.
318
+ # The 'sql' proc takes an expression and returns a new expression.
319
+ attr_reader :converter
320
+
321
+ # If the value we retrieve from the data provided by the external service is nil, we often want to use
322
+ # a default value instead of nil. The `defaulter` is an `IsomorphicProc` where both procs take the resource
323
+ # object and return a default value that is used in the upsert. A common example is the `now` defaulter,
324
+ # which uses the current time as the default value.
325
+ #
326
+ # @return [IsomorphicProc]
327
+ # The 'ruby' proc accepts (resource:, event:, enrichment:, service_integration:) and returns a value.
328
+ # The 'sql' proc accepts (service_integration:) and returns an sql expression.
329
+ attr_reader :defaulter
330
+
331
+ # If `skip_nil` is set to true, we only add the described value to the hash that gets upserted if it is not
332
+ # nil. This is so that we don't override existing data in the database row with a nil value.
333
+ # @return [Boolean]
334
+ attr_reader :skip_nil
335
+ alias skip_nil? skip_nil
336
+
337
+ # If provided, run this before backfilling as part of UPDATE.
338
+ # Usually used to add functions into pg_temp schema.
339
+ # This is an advanced use case; see unit tests for examples.
340
+ attr_reader :backfill_statement
341
+
342
+ # If provided, use this expression as the UPDATE value when adding the column
343
+ # to an existing table.
344
+ # @return [String,Sequel,Sequel::SQL::Expression]
345
+ attr_reader :backfill_expr
346
+
347
+ def initialize(
348
+ name,
349
+ type,
350
+ data_key: nil,
351
+ event_key: nil,
352
+ from_enrichment: false,
353
+ optional: false,
354
+ converter: nil,
355
+ defaulter: nil,
356
+ index: false,
357
+ index_not_null: false,
358
+ skip_nil: false,
359
+ backfill_statement: nil,
360
+ backfill_expr: nil
361
+ )
362
+ raise ArgumentError, "name must be a symbol" unless name.is_a?(Symbol)
363
+ raise ArgumentError, "type #{type.inspect} is not supported" unless COLUMN_TYPES.include?(type)
364
+ raise ArgumentError, "use :tofalse as the defaulter (or nil for no defaulter)" if defaulter == false
365
+ @name = name
366
+ @type = type
367
+ @data_key = data_key || name.to_s
368
+ @event_key = event_key
369
+ @from_enrichment = from_enrichment
370
+ @optional = optional
371
+ @converter = KNOWN_CONVERTERS[converter] || converter
372
+ @defaulter = KNOWN_DEFAULTERS[defaulter] || defaulter
373
+ @index = index
374
+ @index_not_null = index_not_null
375
+ @skip_nil = skip_nil
376
+ @backfill_statement = backfill_statement
377
+ @backfill_expr = backfill_expr
378
+ end
379
+
380
+ def to_dbadapter(**more)
381
+ kw = {name:, type:, index:, backfill_expr:, backfill_statement:}
382
+ kw[:index_where] = Sequel[self.name] !~ nil if self.index_not_null
383
+ kw.merge!(more)
384
+ return Webhookdb::DBAdapter::Column.new(**kw)
385
+ end
386
+
387
+ # Convert this column to an expression that can be used to return
388
+ # the column's value based on what is present in the row.
389
+ # This is generally used to 'backfill' column values
390
+ # from what is in the data and enrichment columns.
391
+ #
392
+ # NOTE: this method assumes Postgres as the backing database.
393
+ # To support others will require additional work and some abstraction.
394
+ def to_sql_expr
395
+ source_col = @from_enrichment ? :enrichment : :data
396
+ source_col_expr = Sequel.pg_json(source_col)
397
+ # Have to use string keys here, PG handles it alright though.
398
+ dkey = @data_key.respond_to?(:to_ary) ? @data_key.map(&:to_s) : @data_key
399
+ expr = case self.type
400
+ # If we're pulling out a normal value from JSON, get it as a 'native' value (not jsonb) (ie, ->> op).
401
+ when TIMESTAMP, DATE, TEXT, INTEGER, BIGINT
402
+ source_col_expr.get_text(dkey)
403
+ else
404
+ # If this is a more complex value, get it as jsonb (ie, -> op).
405
+ # Note that this can be changed by the sql converter.
406
+ source_col_expr[Array(dkey)]
407
+ end
408
+ if self.converter
409
+ if self.converter.sql == NOT_IMPLEMENTED
410
+ msg = "Converter SQL for #{self.name} is not implemented. This column cannot be added after the fact, " \
411
+ "backfill_expr should be set on the column to provide a manual UPDATE/backfill converter, " \
412
+ "or the :sql converter can be implemented (may not be possible or feasible in all cases)."
413
+ raise TypeError, msg
414
+ end
415
+ conv_kwargs = self.converter.sql.arity == 1 ? {} : {json_path: dkey, source_col: source_col_expr}
416
+ expr = self.converter.sql.call(expr, **conv_kwargs)
417
+ end
418
+ pgcol = Webhookdb::DBAdapter::PG::COLTYPE_MAP.fetch(self.type)
419
+ expr = expr.cast(pgcol)
420
+ (expr = Sequel.function(:coalesce, expr, self.defaulter.sql.call)) if self.defaulter
421
+ return expr
422
+ end
423
+
424
+ def to_ruby_value(resource:, event:, enrichment:, service_integration:)
425
+ v = if self.from_enrichment
426
+ self._dig(enrichment, self.data_key, self.optional)
427
+ elsif event && self.event_key
428
+ # Event keys are never optional since any API using them is going to have fixed keys
429
+ self._dig(event, self.event_key, false)
430
+ else
431
+ self._dig(resource, self.data_key, self.optional)
432
+ end
433
+ (v = self.defaulter.ruby.call(resource:, event:, enrichment:, service_integration:)) if self.defaulter && v.nil?
434
+ v = self.converter.ruby.call(v, resource:, event:, enrichment:, service_integration:) if self.converter
435
+ if (self.type == INTEGER_ARRAY) && !v.nil?
436
+ v = Sequel.pg_array(v, "integer")
437
+ elsif (self.type == TEXT_ARRAY) && !v.nil?
438
+ v = Sequel.pg_array(v, "text")
439
+ elsif (self.type == BIGINT_ARRAY) && !v.nil?
440
+ v = Sequel.pg_array(v, "bigint")
441
+ elsif (self.type == TIMESTAMP) && !v.nil?
442
+ # Postgres CANNOT handle timestamps with a 0000 year,
443
+ # even if the actual time it represents is valid (due to timezone offset).
444
+ # Repro with `SELECT '0000-12-31T18:10:00-05:50'::timestamptz`.
445
+ # So if we are in the year 0, represent the time into UTC to get it out of year 0
446
+ # (if it's still invalid, let it error).
447
+ # NOTE: Only worry about times; if the value is a string, it will still error.
448
+ # Let the caller parse the string into a Time to get this special behavior.
449
+ # Time parsing is too loose to do it here.
450
+ v = v.utc if v.is_a?(Time) && v.year.zero?
451
+ end
452
+ # pg_json doesn't handle thie ssuper well in our situation,
453
+ # so JSON must be inserted as a string.
454
+ if (_stringify_json = self.type == OBJECT && !v.nil? && !v.is_a?(String))
455
+ v = v.to_json
456
+ end
457
+ return v
458
+ end
459
+
460
+ def _dig(h, keys, optional)
461
+ v = h
462
+ karr = Array(keys)
463
+ karr.each do |key|
464
+ begin
465
+ v = optional ? v[key] : v.fetch(key)
466
+ rescue KeyError
467
+ raise KeyError, "key not found: '#{key}' in: #{v.keys}"
468
+ rescue NoMethodError => e
469
+ raise NoMethodError, "Element #{key} of #{karr}\n#{e}"
470
+ end
471
+ # allow optional nested values by returning nil as soon as key not found
472
+ # the problem here is that you effectively set all keys in the sequence as optional
473
+ break if optional && v.nil?
474
+ end
475
+ return v
476
+ end
477
+
478
+ def self._assert_regex_converter_type(re)
479
+ return Regexp.new(re) if re.is_a?(String)
480
+ raise ArgumentError, "regexp must be a string, not a Ruby regex, so it can be used in the database verbatim"
481
+ end
482
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "webhookdb/convertkit"
5
+ require "webhookdb/replicator/convertkit_v1_mixin"
6
+
7
+ class Webhookdb::Replicator::ConvertkitBroadcastV1 < Webhookdb::Replicator::Base
8
+ include Appydays::Loggable
9
+ include Webhookdb::Replicator::ConvertkitV1Mixin
10
+
11
+ # @return [Webhookdb::Replicator::Descriptor]
12
+ def self.descriptor
13
+ return Webhookdb::Replicator::Descriptor.new(
14
+ name: "convertkit_broadcast_v1",
15
+ ctor: ->(sint) { Webhookdb::Replicator::ConvertkitBroadcastV1.new(sint) },
16
+ feature_roles: [],
17
+ resource_name_singular: "ConvertKit Broadcast",
18
+ supports_backfill: true,
19
+ api_docs_url: "https://developers.convertkit.com/#list-broadcasts",
20
+ )
21
+ end
22
+
23
+ def _denormalized_columns
24
+ return [
25
+ Webhookdb::Replicator::Column.new(:click_rate, DECIMAL, from_enrichment: true, optional: true),
26
+ Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, index: true),
27
+ Webhookdb::Replicator::Column.new(:open_rate, DECIMAL, from_enrichment: true, optional: true),
28
+ Webhookdb::Replicator::Column.new(:progress, DECIMAL, from_enrichment: true, optional: true),
29
+ Webhookdb::Replicator::Column.new(:recipients, INTEGER, from_enrichment: true, optional: true),
30
+ Webhookdb::Replicator::Column.new(:show_total_clicks, BOOLEAN, from_enrichment: true, optional: true),
31
+ Webhookdb::Replicator::Column.new(:status, TEXT, from_enrichment: true, optional: true),
32
+ Webhookdb::Replicator::Column.new(:subject, TEXT),
33
+ Webhookdb::Replicator::Column.new(:total_clicks, INTEGER, from_enrichment: true, optional: true),
34
+ Webhookdb::Replicator::Column.new(:unsubscribes, INTEGER, from_enrichment: true, optional: true),
35
+ ]
36
+ end
37
+
38
+ def _resource_and_event(request)
39
+ return request.body, nil
40
+ end
41
+
42
+ def _timestamp_column_name
43
+ return :created_at
44
+ end
45
+
46
+ def _update_where_expr
47
+ return self.qualified_table_sequel_identifier[:data] !~ Sequel[:excluded][:data]
48
+ end
49
+
50
+ def _store_enrichment_body?
51
+ return true
52
+ end
53
+
54
+ def _fetch_enrichment(resource, _event, _request)
55
+ broadcast_id = resource.fetch("id")
56
+ url = "https://api.convertkit.com/v3/broadcasts/#{broadcast_id}/stats?api_secret=#{self.service_integration.backfill_secret}"
57
+ response = Webhookdb::Http.get(url, logger: self.logger, timeout: Webhookdb::Convertkit.http_timeout)
58
+ data = response.parsed_response
59
+ return data.dig("broadcast", "stats") || {}
60
+ end
61
+
62
+ def _fetch_backfill_page(_pagination_token, **_kwargs)
63
+ # this endpoint does not have pagination support
64
+ url = "https://api.convertkit.com/v3/broadcasts?api_secret=#{self.service_integration.backfill_secret}"
65
+ response = Webhookdb::Http.get(url, logger: self.logger, timeout: Webhookdb::Convertkit.http_timeout)
66
+ data = response.parsed_response
67
+ return data["broadcasts"], nil
68
+ end
69
+ end