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,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stripe"
4
+ require "webhookdb/replicator/stripe_v1_mixin"
5
+
6
+ class Webhookdb::Replicator::StripeRefundV1 < Webhookdb::Replicator::Base
7
+ include Appydays::Loggable
8
+ include Webhookdb::Replicator::StripeV1Mixin
9
+
10
+ # @return [Webhookdb::Replicator::Descriptor]
11
+ def self.descriptor
12
+ return Webhookdb::Replicator::Descriptor.new(
13
+ name: "stripe_refund_v1",
14
+ ctor: ->(sint) { Webhookdb::Replicator::StripeRefundV1.new(sint) },
15
+ feature_roles: [],
16
+ resource_name_singular: "Stripe Refund",
17
+ supports_webhooks: true,
18
+ supports_backfill: true,
19
+ api_docs_url: "https://stripe.com/docs/api/refunds",
20
+ )
21
+ end
22
+
23
+ def _remote_key_column
24
+ return Webhookdb::Replicator::Column.new(:stripe_id, TEXT, data_key: "id")
25
+ end
26
+
27
+ def _denormalized_columns
28
+ return [
29
+ Webhookdb::Replicator::Column.new(:amount, INTEGER, index: true),
30
+ Webhookdb::Replicator::Column.new(:balance_transaction, TEXT, index: true),
31
+ Webhookdb::Replicator::Column.new(:charge, TEXT, index: true),
32
+ Webhookdb::Replicator::Column.new(:created, TIMESTAMP, index: true, converter: :tsat),
33
+ Webhookdb::Replicator::Column.new(:payment_intent, TEXT, index: true),
34
+ Webhookdb::Replicator::Column.new(:receipt_number, TEXT, index: true),
35
+ Webhookdb::Replicator::Column.new(:source_transfer_reversal, TEXT, index: true),
36
+ Webhookdb::Replicator::Column.new(:status, TEXT),
37
+ Webhookdb::Replicator::Column.new(:transfer_reversal, TEXT, index: true),
38
+ Webhookdb::Replicator::Column.new(
39
+ :updated,
40
+ TIMESTAMP,
41
+ index: true,
42
+ data_key: "created",
43
+ event_key: "created",
44
+ converter: :tsat,
45
+ ),
46
+ ]
47
+ end
48
+
49
+ def _update_where_expr
50
+ return self.qualified_table_sequel_identifier[:updated] < Sequel[:excluded][:updated]
51
+ end
52
+
53
+ def _mixin_backfill_url
54
+ return "https://api.stripe.com/v1/refunds"
55
+ end
56
+
57
+ def _mixin_event_type_names
58
+ return ["charge.refund.updated"]
59
+ end
60
+
61
+ def restricted_key_resource_name = "Charges"
62
+
63
+ def _upsert_webhook(request, upsert: true)
64
+ resource, _event = self._resource_and_event(request)
65
+ object_type = resource.fetch("object")
66
+ return super if object_type == "refund"
67
+
68
+ # We can hit this when processing a 'charges.updated' webhook for refunds.
69
+ # We have to pull a list of refunds from the "charges" webhook.
70
+ # There is a somewhat infuriating nested pagination
71
+ # mechanism here, where the refunds list in the charge takes this form:
72
+ #
73
+ # "refunds": {
74
+ # "object": "list",
75
+ # "data": [],
76
+ # "has_more": false,
77
+ # "url": "/v1/charges/ch_1JG8U9FFYxHXGyKxPaNIdc0b/refunds"
78
+ # }
79
+ #
80
+ # and the `has_more` and `url` fields contain the information that is rquired to kick off
81
+ # a paginated backfill. `has_more` is almost always going to be false, because it should be
82
+ # rare that a charge has more than ten refunds, so we're just going to ignore this concern
83
+ # for now and issue a DeveloperAlert if pagination is required.
84
+ refunds_obj = request.body.dig("data", "object", "refunds")
85
+
86
+ if refunds_obj.fetch("has_more") == true
87
+ Webhookdb::DeveloperAlert.new(
88
+ subsystem: "Stripe Refunds Webhook Error",
89
+ emoji: ":hook:",
90
+ fallback: "Full backfill required for integration #{self.service_integration.opaque_id}",
91
+ fields: [],
92
+ ).emit
93
+ end
94
+
95
+ refunds_obj.fetch("data").each do |b|
96
+ new_request = request.dup
97
+ new_request.body = b
98
+ super(new_request, upsert:)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stripe"
4
+ require "webhookdb/replicator/stripe_v1_mixin"
5
+
6
+ class Webhookdb::Replicator::StripeSubscriptionItemV1 < Webhookdb::Replicator::Base
7
+ include Appydays::Loggable
8
+ include Webhookdb::Replicator::StripeV1Mixin
9
+
10
+ # @return [Webhookdb::Replicator::Descriptor]
11
+ def self.descriptor
12
+ return Webhookdb::Replicator::Descriptor.new(
13
+ name: "stripe_subscription_item_v1",
14
+ ctor: ->(sint) { Webhookdb::Replicator::StripeSubscriptionItemV1.new(sint) },
15
+ feature_roles: [],
16
+ resource_name_singular: "Stripe Subscription Item",
17
+ supports_webhooks: true,
18
+ supports_backfill: true,
19
+ api_docs_url: "https://stripe.com/docs/api/subscription_items",
20
+ )
21
+ end
22
+
23
+ def _remote_key_column
24
+ return Webhookdb::Replicator::Column.new(:stripe_id, TEXT, data_key: "id")
25
+ end
26
+
27
+ def _denormalized_columns
28
+ return [
29
+ Webhookdb::Replicator::Column.new(:created, TIMESTAMP, index: true, converter: :tsat),
30
+ Webhookdb::Replicator::Column.new(:price, TEXT, index: true, data_key: ["price", "id"], optional: true),
31
+ Webhookdb::Replicator::Column.new(:product, TEXT, index: true, data_key: ["price", "product"], optional: true),
32
+ Webhookdb::Replicator::Column.new(:quantity, INTEGER),
33
+ Webhookdb::Replicator::Column.new(:subscription, TEXT, index: true),
34
+ Webhookdb::Replicator::Column.new(
35
+ :updated,
36
+ TIMESTAMP,
37
+ index: true,
38
+ data_key: "created",
39
+ event_key: "created",
40
+ converter: :tsat,
41
+ ),
42
+ ]
43
+ end
44
+
45
+ def _update_where_expr
46
+ return self.qualified_table_sequel_identifier[:updated] < Sequel[:excluded][:updated]
47
+ end
48
+
49
+ def _mixin_backfill_url
50
+ return "https://api.stripe.com/v1/subscription_items"
51
+ end
52
+
53
+ def _mixin_event_type_names
54
+ return []
55
+ end
56
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stripe"
4
+ require "webhookdb/replicator/stripe_v1_mixin"
5
+
6
+ class Webhookdb::Replicator::StripeSubscriptionV1 < Webhookdb::Replicator::Base
7
+ include Appydays::Loggable
8
+ include Webhookdb::Replicator::StripeV1Mixin
9
+
10
+ # @return [Webhookdb::Replicator::Descriptor]
11
+ def self.descriptor
12
+ return Webhookdb::Replicator::Descriptor.new(
13
+ name: "stripe_subscription_v1",
14
+ ctor: ->(sint) { Webhookdb::Replicator::StripeSubscriptionV1.new(sint) },
15
+ feature_roles: [],
16
+ resource_name_singular: "Stripe Subscription",
17
+ supports_webhooks: true,
18
+ supports_backfill: true,
19
+ api_docs_url: "https://stripe.com/docs/api/subscriptions",
20
+ )
21
+ end
22
+
23
+ def _remote_key_column
24
+ return Webhookdb::Replicator::Column.new(:stripe_id, TEXT, data_key: "id")
25
+ end
26
+
27
+ def _denormalized_columns
28
+ return [
29
+ Webhookdb::Replicator::Column.new(:billing_cycle_anchor, TIMESTAMP, index: true, converter: :tsat),
30
+ Webhookdb::Replicator::Column.new(:cancel_at, TIMESTAMP, index: true, optional: true, converter: :tsat),
31
+ Webhookdb::Replicator::Column.new(:canceled_at, TIMESTAMP, index: true, optional: true, converter: :tsat),
32
+ Webhookdb::Replicator::Column.new(:created, TIMESTAMP, index: true, converter: :tsat),
33
+ Webhookdb::Replicator::Column.new(:current_period_end, TIMESTAMP, index: true, converter: :tsat),
34
+ Webhookdb::Replicator::Column.new(:current_period_start, TIMESTAMP, index: true, converter: :tsat),
35
+ Webhookdb::Replicator::Column.new(:customer, TEXT, index: true),
36
+ Webhookdb::Replicator::Column.new(:default_payment_method, TEXT),
37
+ Webhookdb::Replicator::Column.new(:default_source, TEXT),
38
+ Webhookdb::Replicator::Column.new(:discount, TEXT, index: true),
39
+ Webhookdb::Replicator::Column.new(:ended_at, TIMESTAMP, index: true, optional: true),
40
+ Webhookdb::Replicator::Column.new(:latest_invoice, TEXT, index: true),
41
+ Webhookdb::Replicator::Column.new(:schedule, TEXT, index: true),
42
+ Webhookdb::Replicator::Column.new(:start_date, TIMESTAMP, index: true, converter: :tsat),
43
+ Webhookdb::Replicator::Column.new(:status, TEXT),
44
+ Webhookdb::Replicator::Column.new(:trial_end, TIMESTAMP, optional: true, converter: :tsat),
45
+ Webhookdb::Replicator::Column.new(:trial_start, TIMESTAMP, optional: true, converter: :tsat),
46
+ Webhookdb::Replicator::Column.new(
47
+ :updated,
48
+ TIMESTAMP,
49
+ index: true,
50
+ data_key: "created",
51
+ event_key: "created",
52
+ converter: :tsat,
53
+ ),
54
+ ]
55
+ end
56
+
57
+ def _update_where_expr
58
+ return self.qualified_table_sequel_identifier[:updated] < Sequel[:excluded][:updated]
59
+ end
60
+
61
+ def _mixin_backfill_url
62
+ return "https://api.stripe.com/v1/subscriptions"
63
+ end
64
+
65
+ def _mixin_event_type_names
66
+ return [
67
+ "customer.subscription.created",
68
+ "customer.subscription.deleted",
69
+ "customer.subscription.pending_update_applied",
70
+ "customer.subscription.pending_update_expired",
71
+ "customer.subscription.trial_will_end",
72
+ "customer.subscription.updated",
73
+ ]
74
+ end
75
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/stripe"
4
+
5
+ module Webhookdb::Replicator::StripeV1Mixin
6
+ def _resource_and_event(request)
7
+ body = request.body
8
+ return body.fetch("data").fetch("object"), body if body.fetch("object") == "event"
9
+ return body, nil
10
+ end
11
+
12
+ def _mixin_backfill_url
13
+ raise NotImplementedError
14
+ end
15
+
16
+ # this array describes which event this webhook should subscribe to
17
+ # https://stripe.com/docs/api/events/types
18
+ def _mixin_event_type_names
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def _webhook_response(request)
23
+ return Webhookdb::Stripe.webhook_response(request, self.service_integration.webhook_secret)
24
+ end
25
+
26
+ def _timestamp_column_name
27
+ return :updated
28
+ end
29
+
30
+ def calculate_webhook_state_machine
31
+ step = Webhookdb::Replicator::StateMachineStep.new
32
+ unless self.service_integration.webhook_secret.present?
33
+ step.output = %{You are about to start replicating #{self.resource_name_singular} info into WebhookDB.
34
+ We've made an endpoint available for #{self.resource_name_singular} webhooks:
35
+
36
+ #{self._webhook_endpoint}
37
+
38
+ From your Stripe Dashboard, go to Developers -> Webhooks -> Add Endpoint.
39
+ Use the URL above, and choose all of the following events:
40
+ #{self._mixin_event_type_names.join("\n ")}
41
+ Then click Add Endpoint.
42
+
43
+ The page for the webhook will have a 'Signing Secret' section.
44
+ Reveal it, then copy the secret (it will start with `whsec_`).
45
+ }
46
+ return step.secret_prompt("secret").webhook_secret(self.service_integration)
47
+ end
48
+
49
+ step.output = %(Great! WebhookDB is now listening for #{self.resource_name_singular} webhooks.
50
+ #{self._query_help_output}
51
+ In order to backfill existing #{self.resource_name_plural}, run this from a shell:
52
+
53
+ #{self._backfill_command}
54
+ )
55
+ return step.completed
56
+ end
57
+
58
+ def calculate_backfill_state_machine
59
+ step = Webhookdb::Replicator::StateMachineStep.new
60
+ unless self.service_integration.backfill_key.present?
61
+ step.output = %(In order to backfill #{self.resource_name_plural}, we need an API key.
62
+ From your Stripe Dashboard, go to Developers -> API Keys -> Restricted Keys -> Create Restricted Key.
63
+ Create a key with Read access to #{self.restricted_key_resource_name}.
64
+ Submit, then copy the key when Stripe shows it to you:
65
+ )
66
+ return step.secret_prompt("Restricted Key").backfill_key(self.service_integration)
67
+ end
68
+
69
+ unless (result = self.verify_backfill_credentials).verified
70
+ self.service_integration.replicator.clear_backfill_information
71
+ step.output = result.message
72
+ return step.secret_prompt("Restricted Key").backfill_key(self.service_integration)
73
+ end
74
+
75
+ step.output = %(Great! We are going to start backfilling your #{self.resource_name_plural}.
76
+ #{self._query_help_output}
77
+ )
78
+ return step.completed
79
+ end
80
+
81
+ def restricted_key_resource_name = self.resource_name_plural.gsub(/^Stripe /, "")
82
+
83
+ def _verify_backfill_403_err_msg
84
+ return "It looks like that API Key does not have permission to access #{self.resource_name_singular} Records. " \
85
+ "Please check the permissions by going to the list of restricted keys and " \
86
+ "hovering over the information icon in the entry for this key. " \
87
+ "Once you've verified or corrected the permissions for this key, " \
88
+ "please reenter the API Key you just created:"
89
+ end
90
+
91
+ def _verify_backfill_401_err_msg
92
+ return "It looks like that API Key is invalid. Please reenter the API Key you just created:"
93
+ end
94
+
95
+ def _verify_backfill_err_msg
96
+ return "An error occurred. Please reenter the API Key you just created:"
97
+ end
98
+
99
+ def _fetch_backfill_page(pagination_token, **_kwargs)
100
+ url = self._mixin_backfill_url
101
+ url += pagination_token if pagination_token.present?
102
+ response = Webhookdb::Http.get(
103
+ url,
104
+ basic_auth: {username: self.service_integration.backfill_key},
105
+ logger: self.logger,
106
+ timeout: Webhookdb::Stripe.http_timeout,
107
+ )
108
+ data = response.parsed_response
109
+ next_page_param = nil
110
+ if data["has_more"]
111
+ last_item_id = data["data"][-1]["id"]
112
+ next_page_param = "?starting_after=" + last_item_id
113
+ end
114
+ return data["data"], next_page_param
115
+ end
116
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/replicator/transistor_v1_mixin"
4
+
5
+ class Webhookdb::Replicator::TransistorEpisodeStatsV1 < Webhookdb::Replicator::Base
6
+ include Appydays::Loggable
7
+ include Webhookdb::Replicator::TransistorV1Mixin
8
+
9
+ # @return [Webhookdb::Replicator::Descriptor]
10
+ def self.descriptor
11
+ return Webhookdb::Replicator::Descriptor.new(
12
+ name: "transistor_episode_stats_v1",
13
+ ctor: ->(sint) { Webhookdb::Replicator::TransistorEpisodeStatsV1.new(sint) },
14
+ feature_roles: [],
15
+ resource_name_singular: "Transistor Episode Stats",
16
+ resource_name_plural: "Transistor Episode Stats",
17
+ dependency_descriptor: Webhookdb::Replicator::TransistorEpisodeV1.descriptor,
18
+ supports_backfill: true,
19
+ api_docs_url: "https://developers.transistor.fm/#EpisodeAnalytics",
20
+ )
21
+ end
22
+
23
+ CONV_PARSE_DMY_DASH = Webhookdb::Replicator::Column::IsomorphicProc.new(
24
+ ruby: lambda do |s, **_|
25
+ return Date.strptime(s, "%d-%m-%Y")
26
+ rescue TypeError, Date::Error
27
+ return nil
28
+ end,
29
+ sql: ->(e) { Sequel.function(:to_date, e, "DD-MM-YYYY") },
30
+ )
31
+
32
+ CONV_REMOTE_KEY = Webhookdb::Replicator::Column::IsomorphicProc.new(
33
+ ruby: ->(_, resource:, **_) { "#{resource.fetch('episode_id')}-#{resource.fetch('date')}" },
34
+ # Because this is a non-nullable key, we never need this in SQL
35
+ sql: ->(_) { Sequel.lit("'do not use'") },
36
+ )
37
+
38
+ def _remote_key_column
39
+ return Webhookdb::Replicator::Column.new(
40
+ :compound_identity,
41
+ TEXT,
42
+ data_key: "<compound key, see converter>",
43
+ index: true,
44
+ optional: true,
45
+ converter: CONV_REMOTE_KEY,
46
+ )
47
+ end
48
+
49
+ def _denormalized_columns
50
+ return [
51
+ Webhookdb::Replicator::Column.new(:episode_id, TEXT),
52
+ Webhookdb::Replicator::Column.new(:date, DATE, converter: CONV_PARSE_DMY_DASH),
53
+ Webhookdb::Replicator::Column.new(:downloads, INTEGER),
54
+ Webhookdb::Replicator::Column.new(:row_updated_at, TIMESTAMP, defaulter: :now, optional: true),
55
+ ]
56
+ end
57
+
58
+ def _timestamp_column_name
59
+ return :row_updated_at
60
+ end
61
+
62
+ def _resource_and_event(request)
63
+ return request.body, nil
64
+ end
65
+
66
+ def _update_where_expr
67
+ return self.qualified_table_sequel_identifier[:downloads] !~ Sequel[:excluded][:downloads]
68
+ end
69
+
70
+ def calculate_backfill_state_machine
71
+ if (step = self.calculate_dependency_state_machine_step(dependency_help: ""))
72
+ return step
73
+ end
74
+ step = Webhookdb::Replicator::StateMachineStep.new
75
+ step.output = %(Great! That's all the information we need.
76
+ When your Transistor Episodes get added or updated, their stats will be updated in WebhookDB too.
77
+
78
+ #{self._query_help_output})
79
+ return step.completed
80
+ end
81
+
82
+ def _backfillers
83
+ episode_svc = self.service_integration.depends_on.replicator
84
+ backfillers = episode_svc.admin_dataset(timeout: :fast) do |episode_ds|
85
+ episode_ds.select(:transistor_id, :created_at).map do |episode|
86
+ EpisodeStatsBackfiller.new(
87
+ episode_svc:,
88
+ episode_stats_svc: self,
89
+ episode_id: episode[:transistor_id],
90
+ episode_created_at: episode[:created_at],
91
+ )
92
+ end
93
+ end
94
+ return backfillers
95
+ end
96
+
97
+ class EpisodeStatsBackfiller < Webhookdb::Backfiller
98
+ def initialize(episode_svc:, episode_stats_svc:, episode_id:, episode_created_at:)
99
+ @episode_svc = episode_svc
100
+ @episode_stats_svc = episode_stats_svc
101
+ @episode_id = episode_id
102
+ @episode_created_at = episode_created_at
103
+ super()
104
+ end
105
+
106
+ def handle_item(item)
107
+ item["episode_id"] = @episode_id
108
+ @episode_stats_svc.upsert_webhook_body(item)
109
+ end
110
+
111
+ def fetch_backfill_page(_pagination_token, **_kwargs)
112
+ analytics_url = "https://api.transistor.fm/v1/analytics/episodes/" + @episode_id
113
+ # The "downloads" stat gets collected daily but will not change retroactively for a past date.
114
+ # If there are already rows in the enrichment table matching the episode_id, we want to check
115
+ # the date of the last entry so that we don't have to upsert information that we know will not
116
+ # be changed. We allow for a two day buffer before the date of the last entry to account for changes
117
+ # that may occur on the day of a new entry, while the downloads are accruing.
118
+ latest_update = @episode_stats_svc.admin_dataset(timeout: :fast) do |ds|
119
+ ds.where(episode_id: @episode_id).max(:date)
120
+ end
121
+ start_date = latest_update.nil? ? @episode_created_at : (latest_update - 2.days)
122
+ request_body = {
123
+ start_date: start_date.strftime("%d-%m-%Y"),
124
+ end_date: Time.now.strftime("%d-%m-%Y"),
125
+ }
126
+ response = Webhookdb::Http.get(
127
+ analytics_url,
128
+ headers: {"x-api-key" => @episode_svc.service_integration.backfill_key},
129
+ body: request_body,
130
+ logger: @episode_stats_svc.logger,
131
+ timeout: Webhookdb::Transistor.http_timeout,
132
+ )
133
+ data = response.parsed_response.dig("data", "attributes", "downloads") || []
134
+ return data, nil
135
+ end
136
+ end
137
+
138
+ def on_dependency_webhook_upsert(_replicator, _payload, *)
139
+ return
140
+ end
141
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/transistor"
4
+ require "webhookdb/replicator/transistor_v1_mixin"
5
+ require "nokogiri"
6
+
7
+ class Webhookdb::Replicator::TransistorEpisodeV1 < Webhookdb::Replicator::Base
8
+ include Appydays::Loggable
9
+ include Webhookdb::Replicator::TransistorV1Mixin
10
+
11
+ # @return [Webhookdb::Replicator::Descriptor]
12
+ def self.descriptor
13
+ return Webhookdb::Replicator::Descriptor.new(
14
+ name: "transistor_episode_v1",
15
+ ctor: ->(sint) { Webhookdb::Replicator::TransistorEpisodeV1.new(sint) },
16
+ feature_roles: [],
17
+ resource_name_singular: "Transistor Episode",
18
+ supports_backfill: true,
19
+ api_docs_url: "https://developers.transistor.fm/#Episode",
20
+ )
21
+ end
22
+
23
+ def _denormalized_columns
24
+ return [
25
+ Webhookdb::Replicator::Column.new(:author, TEXT, data_key: ["attributes", "author"]),
26
+ Webhookdb::Replicator::Column.new(
27
+ :created_at,
28
+ TIMESTAMP,
29
+ index: true,
30
+ data_key: ["attributes", "created_at"],
31
+ ),
32
+ Webhookdb::Replicator::Column.new(:duration, INTEGER, data_key: ["attributes", "duration"]),
33
+ Webhookdb::Replicator::Column.new(:keywords, TEXT, data_key: ["attributes", "keywords"]),
34
+ Webhookdb::Replicator::Column.new(:number, INTEGER, index: true, data_key: ["attributes", "number"]),
35
+ Webhookdb::Replicator::Column.new(
36
+ :published_at,
37
+ TIMESTAMP,
38
+ index: true,
39
+ data_key: ["attributes", "published_at"],
40
+ ),
41
+ Webhookdb::Replicator::Column.new(:season, INTEGER, index: true, data_key: ["attributes", "season"]),
42
+ Webhookdb::Replicator::Column.new(
43
+ :show_id,
44
+ TEXT,
45
+ index: true,
46
+ data_key: ["relationships", "show", "data", "id"],
47
+ ),
48
+ Webhookdb::Replicator::Column.new(:status, TEXT, data_key: ["attributes", "status"]),
49
+ Webhookdb::Replicator::Column.new(:title, TEXT, data_key: ["attributes", "title"]),
50
+ Webhookdb::Replicator::Column.new(:type, TEXT, data_key: ["attributes", "type"]),
51
+ Webhookdb::Replicator::Column.new(
52
+ :updated_at,
53
+ TIMESTAMP,
54
+ index: true,
55
+ data_key: ["attributes", "updated_at"],
56
+ ),
57
+ # Ideally these would have converters, but they'd be very confusing, and when this was built
58
+ # we only had one transistor user, so we truncated the table instead.
59
+ Webhookdb::Replicator::Column.new(:api_format, INTEGER, optional: true),
60
+ Webhookdb::Replicator::Column.new(:logical_summary, TEXT, optional: true),
61
+ Webhookdb::Replicator::Column.new(:logical_description, TEXT, optional: true),
62
+ ]
63
+ end
64
+
65
+ def _prepare_for_insert(resource, event, request, enrichment)
66
+ h = super
67
+ # Transistor merged their summary and description fields so they're authored
68
+ # as one big 'description' HTML blob in February 2023. Previous to that,
69
+ # there were separate summary and description fields
70
+ # (we call this api_format 1).
71
+ #
72
+ # If we have a nil summary, we know this is a 'new' format (api_format 2).
73
+ # In that case, look for the first line of the HTML,
74
+ # and treat that as the summary. Anything else in the HTML is treated as
75
+ # the remaining description. Some care is paid to whitespace, too,
76
+ # since <br> tags can be used within an element.
77
+ summary = resource.fetch("attributes").fetch("summary", nil)
78
+ description = resource.fetch("attributes").fetch("description", nil)
79
+ if summary.nil?
80
+ h[:api_format] = 2
81
+ parsed_desc = Nokogiri::HTML5.fragment(description)
82
+
83
+ extracted_summary = self._extract_first_html_line_as_text(parsed_desc)
84
+ h[:logical_description] = nil
85
+ if extracted_summary
86
+ h[:logical_summary] = extracted_summary
87
+ h[:logical_description] = parsed_desc.to_s.strip if parsed_desc.inner_text.present?
88
+ else
89
+ h[:logical_summary] = parsed_desc.to_s.strip
90
+ end
91
+ else
92
+ h[:logical_summary] = summary
93
+ h[:logical_description] = description
94
+ h[:api_format] = 1
95
+ end
96
+ return h
97
+ end
98
+
99
+ # Usually the Transistor HTML looks like <div>foo<br><br>hello</div>.
100
+ # Extract 'foo' as text, remove leading <br>, and return <div>hello</div>.
101
+ def _extract_first_html_line_as_text(element)
102
+ # Grab the first div or p element, where the text is.
103
+ first_div = element.css("div, p").first
104
+ return nil unless first_div
105
+ # Iterate over each child element:
106
+ # - If it's a text element, it's part of the first line.
107
+ # - If it's a br/div/p element, we have reached the end of the first line.
108
+ # - Otherwise, it's probably some type of style element, and can be appended.
109
+ first_line_html = +""
110
+ first_div.children.to_a.each do |child|
111
+ if child.is_a?(Nokogiri::XML::Text)
112
+ first_line_html << child.inner_text
113
+ child.remove
114
+ elsif child.name == "br"
115
+ # Remove additional br tags, this is like
116
+ # removing leading whitespace of the new/remaining description.
117
+ while (sibling = child.next)
118
+ break unless sibling.name == "br"
119
+ sibling.remove
120
+ end
121
+ child.remove
122
+ break
123
+ elsif BLOCK_ELEMENT_TAGS.include?(child.name)
124
+ break
125
+ else
126
+ first_line_html << child.to_html
127
+ child.remove
128
+ end
129
+ end
130
+ first_div.remove if first_div.inner_text.blank?
131
+ return first_line_html.strip
132
+ end
133
+
134
+ BLOCK_ELEMENT_TAGS = ["p", "div"].freeze
135
+
136
+ def upsert_has_deps?
137
+ return true
138
+ end
139
+
140
+ def parse_date_from_api(date_string)
141
+ return Time.strptime(date_string, "%d-%m-%Y")
142
+ end
143
+
144
+ def _fetch_backfill_page(pagination_token, last_backfilled:)
145
+ url = "https://api.transistor.fm/v1/episodes"
146
+ pagination_token = 1 if pagination_token.blank?
147
+ response = Webhookdb::Http.get(
148
+ url,
149
+ headers: {"x-api-key" => self.service_integration.backfill_key},
150
+ body: {pagination: {page: pagination_token, per: 500}},
151
+ logger: self.logger,
152
+ timeout: Webhookdb::Transistor.http_timeout,
153
+ )
154
+ data = response.parsed_response
155
+ episodes = data["data"]
156
+ current_page = data["meta"]["currentPage"]
157
+ total_pages = data["meta"]["totalPages"]
158
+ next_page = (current_page.to_i + 1 if current_page < total_pages)
159
+
160
+ if last_backfilled.present?
161
+ earliest_data_created = episodes.empty? ? Time.at(0) : episodes[-1].dig("attributes", "created_at")
162
+ paged_to_already_seen_records = earliest_data_created < last_backfilled
163
+
164
+ return episodes, nil if paged_to_already_seen_records
165
+ end
166
+
167
+ return episodes, next_page
168
+ end
169
+ end