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,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appydays/configurable"
4
+ require "appydays/loggable"
5
+
6
+ module Webhookdb::Plivo
7
+ include Appydays::Configurable
8
+ include Appydays::Loggable
9
+
10
+ configurable(:plivo) do
11
+ setting :http_timeout, 25
12
+ end
13
+
14
+ def self.request(method, tail, auth_id:, auth_token:, body: nil, **options)
15
+ tail = tail.delete_suffix("/")
16
+ url = "https://api.plivo.com/v1/Account/#{auth_id}#{tail}/"
17
+ options[:basic_auth] = {username: auth_id, password: auth_token}
18
+ options[:logger] = self.logger
19
+ if body
20
+ options[:headers] = {"Content-Type" => "application/json"}
21
+ options[:body] = body.to_json
22
+ end
23
+ options[:method] = method if method != :get
24
+ return Webhookdb::Http.send(method, url, **options)
25
+ end
26
+
27
+ def self.webhook_response(request, auth_token)
28
+ raise Webhookdb::InvalidPrecondition, "auth_token cannot be nil/blank" if auth_token.blank?
29
+ # See https://www.plivo.com/docs/sms/xml/request#validation
30
+ # See https://www.plivo.com/docs/sms/concepts/signature-validation#code
31
+ (signature = request.env["HTTP_X_PLIVO_SIGNATURE_V2"]) or
32
+ return Webhookdb::WebhookResponse.error("missing signature")
33
+ (nonce = request.env["HTTP_X_PLIVO_SIGNATURE_V2_NONCE"]) or
34
+ return Webhookdb::WebhookResponse.error("missing nonce")
35
+ url = request.url
36
+ uri = url.split("?")[0]
37
+ ok = self._valid_signature?(uri, nonce, signature, auth_token)
38
+ return ok ? Webhookdb::WebhookResponse.ok : Webhookdb::WebhookResponse.error("invalid signature")
39
+ end
40
+
41
+ # Copied from https://github.com/plivo/plivo-ruby/blob/119038345475c6216bf040926747105b66fd588a/lib/plivo/utils.rb#L213C1-L220C8
42
+ # We do not use the Plivo gem since it is a mess.
43
+ def self._valid_signature?(uri, nonce, signature, auth_token)
44
+ parsed_uri = URI.parse(uri)
45
+ uri_details = {host: parsed_uri.host, path: parsed_uri.path}
46
+ uri_builder_module = parsed_uri.scheme == "https" ? URI::HTTPS : URI::HTTP
47
+ data_to_sign = uri_builder_module.build(uri_details).to_s + nonce
48
+ sha256_digest = OpenSSL::Digest.new("sha256")
49
+ encoded_digest = Base64.encode64(OpenSSL::HMAC.digest(sha256_digest, auth_token, data_to_sign)).strip
50
+ return ActiveSupport::SecurityUtils.secure_compare(encoded_digest, signature)
51
+ end
52
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webhookdb::Postgres::Maintenance
4
+ include Appydays::Configurable
5
+
6
+ configurable(:pg_maintenance) do
7
+ setting :debug, true
8
+ setting :docker, true
9
+ # Match what's available on the server.
10
+ setting :pg_repack_image, "hartmutcouk/pg-repack-docker:1.4.7"
11
+ end
12
+
13
+ class Base
14
+ # @!attribute service_integration
15
+ # @return [Webhookdb::ServiceIntegration]
16
+ attr_reader :service_integration
17
+
18
+ def debug? = Webhookdb::Postgres::Maintenance.debug
19
+
20
+ def initialize(service_integration)
21
+ @service_integration = service_integration
22
+ end
23
+
24
+ def conn_params
25
+ org_uri = URI(self.service_integration.organization.admin_connection_url_raw)
26
+ superuser_url = Webhookdb::Organization::DbBuilder.available_server_urls.find { |u| URI(u).host == org_uri.host }
27
+ raise Webhookdb::InvalidPrecondition, "cannot find superuser url for #{org_uri.host}" if superuser_url.nil?
28
+ sup_uri = URI(superuser_url)
29
+ return {
30
+ user: sup_uri.user,
31
+ password: sup_uri.password,
32
+ host: sup_uri.host,
33
+ port: sup_uri.port,
34
+ database: org_uri.path&.delete_prefix("/"),
35
+ table: self.service_integration.table_name,
36
+ }
37
+ end
38
+ end
39
+
40
+ class Query < Base
41
+ def connstr
42
+ h = self.conn_params
43
+ return "postgres://#{h[:user]}:#{h[:password]}@#{h[:host]}:#{h[:port]}/#{h[:database]}"
44
+ end
45
+
46
+ def query = raise NotImplementedError
47
+
48
+ def fetch
49
+ Sequel.connect(self.connstr) do |c|
50
+ c.fetch(self.query).all
51
+ end
52
+ end
53
+
54
+ def run = raise NotImplementedError
55
+ def run_fmt = raise NotImplementedError
56
+ end
57
+
58
+ class Command < Base
59
+ def docker? = Webhookdb::Postgres::Maintenance.docker
60
+
61
+ def psql_conn_params
62
+ h = self.conn_params
63
+ return [
64
+ "--no-password",
65
+ "-U", h[:user],
66
+ "-h", h[:host],
67
+ "-p", h[:port],
68
+ "--dbname", h[:database],
69
+ ]
70
+ end
71
+
72
+ def command_list = raise NotImplementedError
73
+ def docker_image = raise NotImplementedError
74
+ def extension = nil
75
+
76
+ def docker_preamble
77
+ return [
78
+ "docker",
79
+ "run",
80
+ "-e",
81
+ "PGPASSWORD=#{self.conn_params[:password]}",
82
+ "-it",
83
+ "--rm",
84
+ self.docker_image,
85
+ ]
86
+ end
87
+
88
+ def create_extension_command_list
89
+ a = ["PGPASSWORD=#{self.conn_params[:password]}", "psql"]
90
+ a += self.psql_conn_params
91
+ a << "-c"
92
+ a << "'CREATE EXTENSION IF NOT EXISTS #{self.extension}'"
93
+ return a
94
+ end
95
+
96
+ def command_strings
97
+ c = []
98
+ c << self.shelex(self.create_extension_command_list) if self.extension
99
+ c << self.shelex(self.command_list)
100
+ return c
101
+ end
102
+
103
+ def shelex(a)
104
+ return a.join(" ")
105
+ end
106
+ end
107
+
108
+ class Repack < Command
109
+ def extension = "pg_repack"
110
+
111
+ def docker_image = Webhookdb::Postgres::Maintenance.pg_repack_image
112
+
113
+ def command_list
114
+ c = []
115
+ c += self.docker_preamble if self.docker?
116
+ c << "pg_repack"
117
+ c += self.psql_conn_params
118
+ c += ["--table", self.conn_params[:table]]
119
+ c << "--no-superuser-check"
120
+ c << "--dry-run"
121
+ c += ["--echo", "--elevel=DEBUG"] if self.debug?
122
+ return c
123
+ end
124
+ end
125
+
126
+ class Count < Query
127
+ def query = "SELECT reltuples AS estimate FROM pg_class WHERE relname = '#{self.service_integration.table_name}'"
128
+
129
+ def run
130
+ r = self.fetch
131
+ return r[0][:estimate].to_i
132
+ end
133
+
134
+ def run_fmt = ActiveSupport::NumberHelper.number_to_delimited(self.run.to_i)
135
+ end
136
+
137
+ class Tables < Query
138
+ def query
139
+ return <<~SQL
140
+ SELECT
141
+ relname AS "relation",
142
+ reltuples as "tuples",
143
+ pg_size_pretty(pg_total_relation_size(C .oid)) as "size"
144
+ FROM pg_class C
145
+ LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
146
+ WHERE nspname NOT IN ('pg_catalog', 'information_schema')
147
+ AND C.relkind = 'r'
148
+ AND nspname !~ '^pg_toast'
149
+ ORDER BY C.relname
150
+ SQL
151
+ end
152
+
153
+ def run
154
+ return self.fetch
155
+ end
156
+
157
+ def run_fmt
158
+ rows = self.run
159
+ namejust = rows.map { |r| r[:relation].size }.max
160
+ return rows.map do |r|
161
+ tuples = ActiveSupport::NumberHelper.number_to_delimited(r[:tuples].to_i).rjust(12, " ")
162
+ "#{r[:relation].ljust(namejust + 1, ' ')} #{r[:size].ljust(7, ' ')} #{tuples}"
163
+ end.join("\n")
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appydays/configurable"
4
+ require "appydays/loggable"
5
+ require "pg"
6
+ require "sequel"
7
+ require "tsort"
8
+
9
+ require "webhookdb"
10
+ require "webhookdb/postgres"
11
+ require "webhookdb/postgres/validations"
12
+ require "webhookdb/postgres/model_utilities"
13
+
14
+ # Initialize the Webhookdb::Postgres::Model class as an abstract model class (i.e.,
15
+ # without a default dataset). This prevents it from looking for a table called
16
+ # `models`, and makes inheriting it more straightforward.
17
+ # Thanks to Michael Granger and Jeremy Evans for the suggestion.
18
+ Webhookdb::Postgres::Model = Class.new(Sequel::Model)
19
+ Webhookdb::Postgres::Model.def_Model(Webhookdb::Postgres)
20
+
21
+ class Webhookdb::Postgres::Model
22
+ include Appydays::Configurable
23
+ extend Webhookdb::Postgres::ModelUtilities
24
+ include Appydays::Loggable
25
+
26
+ configurable(:webhookdb_db) do
27
+ setting :uri, "postgres:/webhookdb_test", key: "DATABASE_URL"
28
+
29
+ # rubocop:disable Naming/VariableNumber
30
+ setting :encryption_key_0, "Tc3X6zkxXgZfHE81MFz2EILStV++BuQY"
31
+ # rubocop:enable Naming/VariableNumber
32
+
33
+ setting :extension_schema, "public"
34
+
35
+ after_configured do
36
+ self.connect(self.uri)
37
+ end
38
+ end
39
+
40
+ # Add one or more extension +modules+ to the receiving class. This allows subsystems
41
+ # like Orders, etc. to decorate models outside of their purview
42
+ # without introducing unnecessary dependencies.
43
+ #
44
+ # Each one of the given +modules+ will be included in the receiving model class, and
45
+ # if it also contains a constant called ClassMethods, the model class will be
46
+ # also be extended with it.
47
+ #
48
+ # @example Add order methods to Webhookdb::Customer
49
+ #
50
+ # module Webhookdb::Orders::CustomerExtensions
51
+ #
52
+ # # Add some associations for Order models
53
+ # def included( model )
54
+ # super
55
+ # model.one_to_many :orders, Sequel[:app][:orders]
56
+ # end
57
+ #
58
+ # def first_order
59
+ # self.orders.first
60
+ # end
61
+ #
62
+ # end
63
+ def self.add_extensions(*modules)
64
+ self.logger.info "Adding extensions to %p: %p" % [self, modules]
65
+
66
+ modules.each do |mod|
67
+ include(mod)
68
+ if mod.const_defined?(:ClassMethods)
69
+ submod = mod.const_get(:ClassMethods)
70
+ self.extend(submod)
71
+ end
72
+ if mod.const_defined?(:PrependedMethods)
73
+ submod = mod.const_get(:PrependedMethods)
74
+ prepend(submod)
75
+ end
76
+ end
77
+ end
78
+
79
+ plugin :column_encryption do |enc|
80
+ enc.key 0, self.encryption_key_0
81
+ end
82
+ end
@@ -0,0 +1,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tsort"
4
+ require "pg"
5
+ require "webhookdb/dbutil"
6
+ require "webhookdb/postgres"
7
+
8
+ # A collection of utilities that can be added to model superclasses.
9
+ module Webhookdb::Postgres::ModelUtilities
10
+ include TSort
11
+
12
+ # Extension callback -- register the +model_class+ with Webhookdb::Postgres.
13
+ def self.extended(model_class)
14
+ super
15
+
16
+ # Sequel::Model API -- load some plugins
17
+ model_class.plugin(:dirty)
18
+ model_class.plugin(:json_serializer)
19
+ model_class.plugin(:many_through_many)
20
+ model_class.plugin(:subclasses)
21
+ model_class.plugin(:tactical_eager_loading)
22
+ model_class.plugin(:update_or_create)
23
+ model_class.plugin(:validation_helpers)
24
+
25
+ model_class.include(Appydays::Loggable)
26
+ model_class.extend(ClassMethods)
27
+ model_class.include(InstanceMethods)
28
+ model_class.dataset_module(DatasetMethods)
29
+ model_class.include(Webhookdb::Postgres::Validations)
30
+
31
+ Webhookdb::Postgres.register_model_superclass(model_class)
32
+ end
33
+
34
+ module ClassMethods
35
+ # The application name, set on database connections.
36
+ attr_reader :appname
37
+
38
+ # Connect to the given URI using the standard extensions and other options.
39
+ def connect(uri)
40
+ newdb = Sequel.connect(
41
+ uri,
42
+ logger: self.logger,
43
+ extensions: [
44
+ :is_distinct_from,
45
+ :pagination,
46
+ :pg_json,
47
+ :pg_inet,
48
+ :pg_array,
49
+ :pg_streaming,
50
+ :pg_range,
51
+ :pg_interval,
52
+ :pg_triggers,
53
+ :pretty_table,
54
+ ],
55
+ **Webhookdb::Dbutil.configured_connection_options,
56
+ )
57
+ self.db = newdb
58
+ self.descendents.each do |subclass|
59
+ subclass.db = newdb
60
+ end
61
+ end
62
+
63
+ # Set the PostgreSQL application name to +name+ to allow per-application connection
64
+ # tracking and other fun stuff.
65
+ def appname=(name)
66
+ @appname = name
67
+ self.update_connection_appname
68
+ end
69
+
70
+ # Set the connection's application name if there is one.
71
+ def update_connection_appname
72
+ return unless self.db
73
+ self.logger.debug "Setting application name to %p" % [self.appname]
74
+ self.db.synchronize do |conn|
75
+ escaped = conn.escape_string(self.appname)
76
+ conn.exec("SET application_name TO '%s'" % [escaped])
77
+ end
78
+ end
79
+
80
+ # Fetch a model class by its +classname+. This can be the fully-qualified
81
+ # name, or just the bit after 'Webhookdb::'.
82
+ def by_name(classname)
83
+ return self.descendents.find do |cl|
84
+ cl.name&.end_with?(classname)
85
+ end
86
+ end
87
+
88
+ # Return the Array of the schemas used by all descendents of the receiving
89
+ # model class.
90
+ def all_loaded_schemas
91
+ return self.descendents.map(&:schema_name).uniq.compact
92
+ end
93
+
94
+ # Create a new schema named +name+ (if it doesn't already exist).
95
+ def create_schema(name, &block)
96
+ self.db.create_schema(name, if_not_exists: true)
97
+ self.instance_eval(&block) if block
98
+ end
99
+
100
+ # Create the schema named +name+, dropping any previous schema by the same name.
101
+ def create_schema!(name, &)
102
+ self.drop_schema!(name)
103
+ self.create_schema(name, &)
104
+ end
105
+
106
+ # Drop the empty schema named +name+ (if it exists).
107
+ def drop_schema(name)
108
+ self.db.drop_schema(name, if_exists: true)
109
+ end
110
+
111
+ # Drop the schema named +name+ and all of its tables.
112
+ def drop_schema!(name)
113
+ self.db.drop_schema(name, if_exists: true, cascade: true)
114
+ end
115
+
116
+ # Returns +true+ if a schema named +name+ exists.
117
+ def schema_exists?(name=self.schema_name)
118
+ ds = self.db[Sequel[:pg_catalog][:pg_namespace]].
119
+ filter(nspname: name.to_s).
120
+ select(:nspname)
121
+
122
+ return ds.first ? true : false
123
+ end
124
+
125
+ # Return the name of the schema the receiving class is in.
126
+ def schema_name
127
+ schemaname, = self.db.send(:schema_and_table, self.table_name)
128
+ return schemaname
129
+ end
130
+
131
+ def now_sql
132
+ return Webhookdb::Postgres.now_sql
133
+ end
134
+
135
+ def extension_schema
136
+ return "public"
137
+ end
138
+
139
+ def install_all_extensions
140
+ extensions = [
141
+ "citext",
142
+ "pgcrypto",
143
+ "btree_gist",
144
+ "pg_trgm",
145
+ ]
146
+ extensions.each do |ext|
147
+ self.db.execute("CREATE EXTENSION IF NOT EXISTS #{ext} WITH SCHEMA #{self.extension_schema}")
148
+ end
149
+ end
150
+
151
+ # TSort API -- yield each model class.
152
+ def tsort_each_node(&)
153
+ self.descendents.select(&:name).each(&)
154
+ end
155
+
156
+ # TSort API -- yield each of the given +model_class+'s dependent model
157
+ # classes.
158
+ def tsort_each_child(model_class)
159
+ # Include (non-anonymous) parents other than Model
160
+ non_anon_parents = model_class.ancestors[1..].
161
+ select { |cl| cl < self }.
162
+ select(&:name)
163
+ # rubocop:disable Style/ExplicitBlockArgument
164
+ non_anon_parents.each do |parentclass|
165
+ yield(parentclass)
166
+ end
167
+ # rubocop:enable Style/ExplicitBlockArgument
168
+
169
+ # Include associated classes for which this model class's table has a
170
+ # foreign key
171
+ model_class.association_reflections.each do |name, config|
172
+ next if config[:polymorphic]
173
+
174
+ associated_class = Object.const_get(config[:class_name])
175
+
176
+ if config[:type] == :many_to_one
177
+ self.logger.debug " %p#%s is dependent on %p" %
178
+ [model_class, name, associated_class]
179
+ yield(associated_class)
180
+ else
181
+ self.logger.debug " %p#%s is *not* dependent on %p" %
182
+ [model_class, name, associated_class]
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ # Like +find_or_create+, but will +find+ again if the +create+
189
+ # call fails due to a +Sequel::UniqueConstraintViolation+,
190
+ # which is usually caused by a race condition.
191
+ def find_or_create_or_find(params, &)
192
+ # Set a savepoint, because the DB error will abort the current transaction.
193
+ self.db.transaction(savepoint: true) do
194
+ return self.find_or_create(params, &)
195
+ end
196
+ rescue Sequel::UniqueConstraintViolation
197
+ return self.find(params)
198
+ end
199
+
200
+ module InstanceMethods
201
+ # Return a human-readable representation of the object as a String suitable for debugging.
202
+ def inspect
203
+ values = self.values.reject do |k, v|
204
+ v.blank? || k.to_s.end_with?("_currency")
205
+ end
206
+ begin
207
+ encrypted = self.class.send(:column_encryption_metadata).to_set { |(col, _)| col.to_s }
208
+ rescue NoMethodError
209
+ encrypted = Set.new
210
+ end
211
+ values = values.map do |(k, v)|
212
+ k = k.to_s
213
+ v = if v.is_a?(Time)
214
+ self.inspect_time(v)
215
+ elsif v.respond_to?(:db_type) && v.db_type.to_s == "tstzrange"
216
+ "%s%s...%s%s" % [
217
+ v.exclude_begin? ? "(" : "[",
218
+ v.begin ? self.inspect_time(v.begin) : "nil",
219
+ v.end ? self.inspect_time(v.end) : "nil",
220
+ v.exclude_end? ? ")" : "]",
221
+ ]
222
+ elsif k.end_with?("_cents")
223
+ accessor = k.match(/^([a-z_]+)_cents/)[1]
224
+ k = accessor
225
+ self.send(accessor).format
226
+ elsif encrypted.include?(k)
227
+ # Render encrypted fields as xyz...abc, or if a URL, hide the user/password.
228
+ unenc = self.send(k)
229
+ if unenc.length < 10
230
+ "\"...\""
231
+ elsif unenc.include?("://")
232
+ uri = URI(unenc)
233
+ uri.user = "*"
234
+ uri.password = "*"
235
+ uri.to_s.inspect
236
+ else
237
+ "\"#{unenc[..2]}...#{unenc[-3..]}\""
238
+ end
239
+ else
240
+ v.inspect
241
+ end
242
+ "#{k}: #{v}"
243
+ end
244
+ return "#<%p %s>" % [self.class, values.join(", ")]
245
+ end
246
+
247
+ def inspect_time(t)
248
+ return t.in_time_zone(Time.zone).strftime("%Y-%m-%d %H:%M:%S")
249
+ end
250
+
251
+ # Return the objects validation errors as full messages joined with commas.
252
+ def error_messages
253
+ return self.errors.full_messages.join(", ")
254
+ end
255
+
256
+ # Return the string used as a topic for events sent from the receiving object.
257
+ def event_prefix
258
+ prefix = self.class.name or return # No events for anonymous classes
259
+ return prefix.gsub("::", ".").downcase
260
+ end
261
+
262
+ # Publish an event from the receiving object of the specified +type+ and with the given +payload+.
263
+ # This does *not* wait for the transaction to complete, so subscribers may not be able to observe
264
+ # any model changes in the database. You probably want to use published_deferred.
265
+ def publish_immediate(type, *payload)
266
+ prefix = self.event_prefix or return
267
+ Amigo.publish(prefix + "." + type.to_s, *payload)
268
+ end
269
+
270
+ # Publish an event in the current db's/transaction's +after_commit+ hook.
271
+ def publish_deferred(type, *payload)
272
+ Webhookdb::Postgres.defer_after_commit(self.db) do
273
+ self.publish_immediate(type, *payload)
274
+ end
275
+ end
276
+
277
+ # Take an exclusive lock on the receiver, ensuring nothing else has updated the object in the meantime.
278
+ # If the updated_at changed from what's on the receiver, to after it acquired the lock, raise LockFailed.
279
+ # Save changes and touch updated_at after calling the given block.
280
+ def resource_lock!
281
+ self.db.transaction do
282
+ old_updated = self.round_time(self.updated_at)
283
+ self.lock!
284
+ new_updated = self.round_time(self.updated_at)
285
+ raise Webhookdb::LockFailed if old_updated != new_updated
286
+ result = yield(self)
287
+ self.updated_at = Time.now
288
+ self.save_changes
289
+ return result
290
+ end
291
+ end
292
+
293
+ # Round +Time+ t to remove nanoseconds, since Postgres can only store microseconds.
294
+ protected def round_time(t)
295
+ return nil if t.nil?
296
+ return t.change(nsec: t.usec * 1000)
297
+ end
298
+
299
+ protected def now_sql
300
+ return Webhookdb::Postgres.now_sql
301
+ end
302
+
303
+ protected def prep_amigo_payload(data)
304
+ # Job arguments to Amigo::Router must be native JSON types, so we do some preprocessing
305
+ data.to_h do |key, value|
306
+ # Empty PG Ranges cannot be converted to ruby types for some reason, we substitute nil
307
+ value_is_empty_range = value.is_a?(Sequel::Postgres::PGRange) && value.empty?
308
+ new_value = value_is_empty_range ? nil : value.as_json
309
+ [key, new_value]
310
+ end
311
+ end
312
+
313
+ # Sequel hook -- send an asynchronous event after the model is saved.
314
+ def after_create
315
+ super
316
+ self.publish_deferred("created", self.id, prep_amigo_payload(self.values))
317
+ end
318
+
319
+ # Sequel hook -- send an asynchronous event after the save is committed.
320
+ def after_update
321
+ super
322
+ self.publish_deferred("updated", self.id, prep_amigo_payload(self.previous_changes))
323
+ end
324
+
325
+ # Sequel hook -- send an event after a transaction that destroys the object is committed.
326
+ def after_destroy
327
+ super
328
+ self.publish_deferred("destroyed", self.id, prep_amigo_payload(self.values))
329
+ end
330
+ end
331
+
332
+ module DatasetMethods
333
+ # Helper for applying multiple conditions for Sequel, where some can be nil.
334
+ def reduce_expr(op_symbol, operands, method: :where)
335
+ return Webhookdb::Dbutil.reduce_expr(self, op_symbol, operands, method:)
336
+ end
337
+
338
+ # Call a block for each row in a dataset.
339
+ # This is the same as paged_each or use_cursor.each, except that for each page,
340
+ # rows are re-fetched using self.where(primary_key => [pks]).all to enable eager loading.
341
+ #
342
+ # (Note that paged_each does not do eager loading, which makes enumerating model associations very slow)
343
+ def each_cursor_page(page_size: 500, order: :id, &block)
344
+ raise LocalJumpError unless block
345
+ raise "dataset requires a use_cursor method, class may need `extension(:pagination)`" unless
346
+ self.respond_to?(:use_cursor)
347
+ model = self.model
348
+ pk = model.primary_key
349
+ current_chunk_pks = []
350
+ order = [order] unless order.respond_to?(:to_ary)
351
+ self.naked.select(pk).order(*order).use_cursor(rows_per_fetch: page_size, hold: true).each do |row|
352
+ current_chunk_pks << row[pk]
353
+ next if current_chunk_pks.length < page_size
354
+ page = model.where(pk => current_chunk_pks).order(*order).all
355
+ current_chunk_pks.clear
356
+ page.each(&block)
357
+ end
358
+ model.where(pk => current_chunk_pks).order(*order).all.each(&block)
359
+ end
360
+
361
+ # See each_cursor_page, but takes an additional action on each chunk of returned rows.
362
+ # The action is called with pages of return values from the block when a page is is reached.
363
+ # Each call to action should return nil, a result, or an array of results (nil results are ignored).
364
+ #
365
+ # The most common case is for ETL: process one dataset, map it in a block to return new row values,
366
+ # and multi_insert it into a different table.
367
+ def each_cursor_page_action(action:, page_size: 500, order: :id)
368
+ raise LocalJumpError unless block_given?
369
+ returned_rows_chunk = []
370
+ self.each_cursor_page(page_size:, order:) do |instance|
371
+ new_row = yield(instance)
372
+ next if action.nil? || new_row.nil?
373
+ new_row.respond_to?(:to_ary) ? returned_rows_chunk.concat(new_row) : returned_rows_chunk.push(new_row)
374
+ if returned_rows_chunk.length >= page_size
375
+ action.call(returned_rows_chunk)
376
+ returned_rows_chunk.clear
377
+ end
378
+ end
379
+ action&.call(returned_rows_chunk)
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/postgres/model"
4
+ require "sequel/plugins/money_fields"
5
+ require "sequel/plugins/tstzrange_fields"
6
+
7
+ # A placeholder model for the magical mixin classes (imaged, ticketable, annotated).
8
+ # We can't easily test these things with non-Webhookdb::Postgres::Model subclasses,
9
+ # and we can't easily define Webhookdb::Postgres::Model subclasses as part of a spec/transaction.
10
+ # Even if Sequel can handle it, it creates some global state issues.
11
+ # Make sure this class is only required during specs, and not normal gem usage.
12
+ # It should never have a persistent table; only a table in the test database.
13
+ class Webhookdb::Postgres::TestingPixie < Webhookdb::Postgres::Model(:testing_pixies)
14
+ plugin :money_fields, :price_per_unit
15
+ plugin :tstzrange_fields, :active_during
16
+ end