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,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appydays/configurable"
4
+ require "bcrypt"
5
+ require "openssl"
6
+ require "webhookdb/id"
7
+ require "webhookdb/postgres/model"
8
+ require "webhookdb/demo_mode"
9
+
10
+ class Webhookdb::Customer < Webhookdb::Postgres::Model(:customers)
11
+ extend Webhookdb::MethodUtilities
12
+ include Appydays::Configurable
13
+
14
+ class InvalidPassword < StandardError; end
15
+ class SignupDisabled < StandardError; end
16
+
17
+ configurable(:customer) do
18
+ setting :signup_email_allowlist, ["*"], convert: ->(s) { s.split }
19
+ setting :skip_authentication, false
20
+ setting :skip_authentication_allowlist, [], convert: ->(s) { s.split }
21
+ end
22
+
23
+ # The bcrypt hash cost. Changing this would invalidate all passwords!
24
+ # It's only here so we can change it for testing.
25
+ singleton_attr_accessor :password_hash_cost
26
+ @password_hash_cost = 10
27
+
28
+ MIN_PASSWORD_LENGTH = 8
29
+
30
+ # A bcrypt digest that's valid, but not a real digest. Used as a placeholder for
31
+ # accounts with no passwords, which makes them impossible to authenticate. Or at
32
+ # least much less likely than with a random string.
33
+ PLACEHOLDER_PASSWORD_DIGEST = "$2a$11$....................................................."
34
+
35
+ # Regex that matches the prefix of a deleted user's email
36
+ DELETED_EMAIL_PATTERN = /^(?<prefix>\d+(?:\.\d+)?)\+(?<rest>.*)$/
37
+
38
+ plugin :timestamps
39
+ plugin :soft_deletes
40
+
41
+ one_to_many :all_memberships, class: "Webhookdb::OrganizationMembership"
42
+ one_to_many :invited_memberships,
43
+ class: "Webhookdb::OrganizationMembership",
44
+ conditions: {verified: false},
45
+ adder: ->(om) { om.update(customer_id: id, verified: false) }
46
+ one_to_many :verified_memberships,
47
+ class: "Webhookdb::OrganizationMembership",
48
+ conditions: {verified: true},
49
+ adder: ->(om) { om.update(customer_id: id, verified: true) }
50
+ one_to_one :default_membership, class: "Webhookdb::OrganizationMembership", conditions: {is_default: true}
51
+ one_to_many :message_deliveries, key: :recipient_id, class: "Webhookdb::Message::Delivery"
52
+ one_to_many :reset_codes, class: "Webhookdb::Customer::ResetCode", order: Sequel.desc([:created_at])
53
+ many_to_many :roles, class: "Webhookdb::Role", join_table: :roles_customers
54
+
55
+ dataset_module do
56
+ def with_email(*emails)
57
+ emails = emails.map { |e| e.downcase.strip }
58
+ return self.where(email: emails)
59
+ end
60
+ end
61
+
62
+ def self.with_email(e)
63
+ return self.dataset.with_email(e).first
64
+ end
65
+
66
+ def self.find_or_create_for_email(email)
67
+ email = email.strip.downcase
68
+ # If there is no Customer object associated with the email, create one
69
+ me = Webhookdb::Customer[email:]
70
+ return [false, me] if me
71
+ signup_allowed = self.signup_email_allowlist.any? { |pattern| File.fnmatch(pattern, email) }
72
+ raise SignupDisabled unless signup_allowed
73
+ return [true, Webhookdb::Customer.create(email:, password: SecureRandom.hex(32))]
74
+ end
75
+
76
+ # Make sure the customer has a default organization.
77
+ # New registrants, or users who have been invited (so have an existing customer and invited org)
78
+ # get an org created. Default orgs must already be verified as per a DB constraint.
79
+ # @return [Array<TrueClass,FalseClass,Webhookdb::OrganizationMembership>] Tuple of [created, membership]
80
+ def self.find_or_create_default_organization(customer)
81
+ mem = customer.default_membership
82
+ return [false, mem] if mem
83
+ email = customer.email
84
+ # We could have no default, but already be in an organization, like if the default was deleted.
85
+ mem = customer.verified_memberships.first
86
+ return [false, mem] if mem
87
+ # We have no verified orgs, so create one.
88
+ # TODO: this will fail if not unique. We need to make sure we pick a unique name/key.
89
+ self_org = Webhookdb::Organization.create(name: "#{email} Org", billing_email: email.to_s)
90
+ mem = customer.add_membership(
91
+ organization: self_org, membership_role: Webhookdb::Role.admin_role, verified: true, is_default: true,
92
+ )
93
+ self_org.publish_deferred("syncdemodata", self_org.id) if Webhookdb::DemoMode.example_datasets_enabled
94
+ return [true, mem]
95
+ end
96
+
97
+ # @return Tuple of <Step, Customer>.
98
+ def self.register_or_login(email:)
99
+ self.db.transaction do
100
+ customer_created, me = self.find_or_create_for_email(email)
101
+ org_created, _membership = self.find_or_create_default_organization(me)
102
+ me.reset_codes_dataset.usable.each(&:expire!)
103
+ me.add_reset_code(transport: "email")
104
+ step = Webhookdb::Replicator::StateMachineStep.new
105
+ step.output = if customer_created || org_created
106
+ %(To finish registering, please look for an email we just sent to #{email}.
107
+ It contains a One Time Password code to validate your email.
108
+ )
109
+ else
110
+ %(Hello again!
111
+
112
+ To finish logging in, please look for an email we just sent to #{email}.
113
+ It contains a One Time Password used to log in.
114
+ )
115
+ end
116
+ step.output += %(You can enter it here, or if you want to finish up from a new prompt, use:
117
+
118
+ webhookdb auth login --username=#{email} --token=<#{Webhookdb::Customer::ResetCode::TOKEN_LENGTH} digit token>
119
+ )
120
+ step.prompt = "Enter the token from your email:"
121
+ step.prompt_is_secret = true
122
+ step.needs_input = true
123
+ step.post_to_url = "/v1/auth"
124
+ step.post_params = {email:}
125
+ step.post_params_value_key = "token"
126
+ return [step, me]
127
+ end
128
+ end
129
+
130
+ # @return Tuple of <Step, Customer>. Customer is nil if token was invalid.
131
+ def self.finish_otp(me, token:)
132
+ if me.nil?
133
+ step = Webhookdb::Replicator::StateMachineStep.new
134
+ step.output = %(Sorry, no one with that email exists. Try running:
135
+
136
+ webhookdb auth login [email]
137
+ )
138
+ step.needs_input = false
139
+ step.complete = true
140
+ step.error_code = "email_not_exist"
141
+ return [step, nil]
142
+ end
143
+
144
+ unless me.should_skip_authentication?
145
+ begin
146
+ Webhookdb::Customer::ResetCode.use_code_with_token(token) do |code|
147
+ raise Webhookdb::Customer::ResetCode::Unusable unless code.customer === me
148
+ code.customer.save_changes
149
+ me.refresh
150
+ end
151
+ rescue Webhookdb::Customer::ResetCode::Unusable
152
+ step = Webhookdb::Replicator::StateMachineStep.new
153
+ step.output = %(Sorry, that token is invalid. Please try again.
154
+ If you have not gotten a code, use Ctrl+C to close this prompt and request a new code:
155
+
156
+ webhookdb auth login #{me.email}
157
+ )
158
+ step.error_code = "invalid_otp"
159
+ step.prompt_is_secret = true
160
+ step.prompt = "Enter the token from your email:"
161
+ step.needs_input = true
162
+ step.post_to_url = "/v1/auth"
163
+ step.post_params = {email: me.email}
164
+ step.post_params_value_key = "token"
165
+ return [step, nil]
166
+ end
167
+ end
168
+
169
+ welcome_tutorial = "Quick tip: Use `webhookdb services list` to see what services are available."
170
+ if me.invited_memberships.present?
171
+ welcome_tutorial = "You have the following pending invites:\n\n" +
172
+ me.invited_memberships.map { |om| " #{om.organization.display_string}: #{om.invitation_code}" }.join("\n") +
173
+ "\n\nUse `webhookdb org join [code]` to accept an invitation."
174
+ end
175
+ step = Webhookdb::Replicator::StateMachineStep.new
176
+ step.output = %(Welcome! For help getting started, please check out
177
+ our docs at https://docs.webhookdb.com.
178
+
179
+ #{welcome_tutorial})
180
+ step.needs_input = false
181
+ step.complete = true
182
+ return [step, me]
183
+ end
184
+
185
+ # If the SKIP_PHONE|EMAIL_VERIFICATION are set, verify the phone/email.
186
+ # Also verify phone and email if the customer email matches the allowlist.
187
+ def should_skip_authentication?
188
+ return true if self.class.skip_authentication
189
+ return true if self.class.skip_authentication_allowlist.any? { |pattern| File.fnmatch(pattern, self.email) }
190
+ return false
191
+ end
192
+
193
+ def ensure_role(role_or_name)
194
+ role = role_or_name.is_a?(Webhookdb::Role) ? role_or_name : Webhookdb::Role[name: role_or_name]
195
+ raise "No role for #{role_or_name}" unless role.present?
196
+ self.add_role(role) unless self.roles_dataset[role.id]
197
+ end
198
+
199
+ def admin?
200
+ return self.roles.include?(Webhookdb::Role.admin_role)
201
+ end
202
+
203
+ def greeting
204
+ return self.name.present? ? self.name : "there"
205
+ end
206
+
207
+ #
208
+ # :section: Memberships
209
+ #
210
+
211
+ def add_membership(opts={})
212
+ if !opts.is_a?(Webhookdb::OrganizationMembership) && !opts.key?(:verified)
213
+ raise ArgumentError, "must pass :verified or a model into add_membership, it is ambiguous otherwise"
214
+ end
215
+ self.associations.delete(opts[:verified] ? :verified_memberships : :invited_memberships)
216
+ return self.add_all_membership(opts)
217
+ end
218
+
219
+ def verified_member_of?(org)
220
+ return !org.verified_memberships_dataset.where(customer_id: self.id).empty?
221
+ end
222
+
223
+ def default_organization
224
+ return self.default_membership&.organization
225
+ end
226
+
227
+ def replace_default_membership(new_mem)
228
+ self.verified_memberships_dataset.update(is_default: false)
229
+ self.associations.delete(:verified_memberships)
230
+ new_mem.update(is_default: true)
231
+ end
232
+
233
+ #
234
+ # :section: Password
235
+ #
236
+
237
+ ### Fetch the user's password as an BCrypt::Password object.
238
+ def encrypted_password
239
+ digest = self.password_digest or return nil
240
+ return BCrypt::Password.new(digest)
241
+ end
242
+
243
+ ### Set the password to the given +unencrypted+ String.
244
+ def password=(unencrypted)
245
+ if unencrypted
246
+ self.check_password_complexity(unencrypted)
247
+ self.password_digest = BCrypt::Password.create(unencrypted, cost: self.class.password_hash_cost)
248
+ else
249
+ self.password_digest = BCrypt::Password.new(PLACEHOLDER_PASSWORD_DIGEST)
250
+ end
251
+ end
252
+
253
+ ### Attempt to authenticate the user with the specified +unencrypted+ password. Returns
254
+ ### +true+ if the password matched.
255
+ def authenticate(unencrypted)
256
+ return false unless unencrypted
257
+ return false if self.soft_deleted?
258
+ return self.encrypted_password == unencrypted
259
+ end
260
+
261
+ protected def new_password_matches?(unencrypted)
262
+ existing_pw = BCrypt::Password.new(self.password_digest)
263
+ new_pw = self.digest_password(unencrypted)
264
+ return existing_pw == new_pw
265
+ end
266
+
267
+ ### Raise if +unencrypted+ password does not meet complexity requirements.
268
+ protected def check_password_complexity(unencrypted)
269
+ raise Webhookdb::Customer::InvalidPassword, "password must be at least %d characters." % [MIN_PASSWORD_LENGTH] if
270
+ unencrypted.length < MIN_PASSWORD_LENGTH
271
+ end
272
+
273
+ #
274
+ # :section: Phone
275
+ #
276
+
277
+ def us_phone
278
+ return Phony.format(self.phone, format: :national)
279
+ end
280
+
281
+ def us_phone=(s)
282
+ self.phone = Webhookdb::PhoneNumber::US.normalize(s)
283
+ end
284
+
285
+ def unverified?
286
+ return !self.email_verified? && !self.phone_verified?
287
+ end
288
+
289
+ #
290
+ # :section: Sequel Hooks
291
+ #
292
+
293
+ def before_create
294
+ self[:opaque_id] ||= Webhookdb::Id.new_opaque_id("cus")
295
+ end
296
+
297
+ ### Soft-delete hook -- prep the user for deletion.
298
+ def before_soft_delete
299
+ self.email = "#{Time.now.to_f}+#{self[:email]}"
300
+ self.password = "aA1!#{SecureRandom.hex(8)}"
301
+ super
302
+ end
303
+
304
+ ### Soft-delete hook -- expire unused, unexpired reset codes and
305
+ ### trigger an event on removal.
306
+
307
+ #
308
+ # :section: Sequel Validation
309
+ #
310
+
311
+ def validate
312
+ super
313
+ self.validates_presence(:email)
314
+ self.validates_format(/[[:graph:]]+@[[:graph:]]+\.[a-zA-Z]{2,}/, :email)
315
+ self.validates_unique(:email)
316
+ self.validates_operator(:==, self.email&.downcase&.strip, :email)
317
+ end
318
+ end
319
+
320
+ # Table: customers
321
+ # -----------------------------------------------------------------------------------------------------------------------------------------------------
322
+ # Columns:
323
+ # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
324
+ # created_at | timestamp with time zone | NOT NULL DEFAULT now()
325
+ # updated_at | timestamp with time zone |
326
+ # soft_deleted_at | timestamp with time zone |
327
+ # password_digest | text | NOT NULL
328
+ # email | citext | NOT NULL
329
+ # name | text | NOT NULL DEFAULT ''::text
330
+ # note | text | NOT NULL DEFAULT ''::text
331
+ # opaque_id | text | NOT NULL
332
+ # Indexes:
333
+ # customers_pkey | PRIMARY KEY btree (id)
334
+ # customers_email_key | UNIQUE btree (email)
335
+ # customers_opaque_id_key | UNIQUE btree (opaque_id)
336
+ # Check constraints:
337
+ # lowercase_nospace_email | (email::text = btrim(lower(email::text)))
338
+ # Referenced By:
339
+ # backfill_jobs | backfill_jobs_created_by_id_fkey | (created_by_id) REFERENCES customers(id) ON DELETE SET NULL
340
+ # customer_reset_codes | customer_reset_codes_customer_id_fkey | (customer_id) REFERENCES customers(id) ON DELETE CASCADE
341
+ # message_deliveries | message_deliveries_recipient_id_fkey | (recipient_id) REFERENCES customers(id) ON DELETE SET NULL
342
+ # organization_database_migrations | organization_database_migrations_started_by_id_fkey | (started_by_id) REFERENCES customers(id) ON DELETE SET NULL
343
+ # organization_memberships | organization_memberships_customer_id_fkey | (customer_id) REFERENCES customers(id)
344
+ # roles_customers | roles_customers_customer_id_fkey | (customer_id) REFERENCES customers(id)
345
+ # sync_targets | sync_targets_created_by_id_fkey | (created_by_id) REFERENCES customers(id) ON DELETE SET NULL
346
+ # webhook_subscriptions | webhook_subscriptions_created_by_id_fkey | (created_by_id) REFERENCES customers(id)
347
+ # -----------------------------------------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/postgres/model"
4
+
5
+ # Simple model for jamming stuff into the database.
6
+ # Since WebhookDB isn't a resource-heavy application,
7
+ # and it's meant to be self-hosted,
8
+ # we may as well do this over pulling in an S3 or GCS dependeency.
9
+ class Webhookdb::DatabaseDocument < Webhookdb::Postgres::Model(:database_documents)
10
+ include Appydays::Configurable
11
+ configurable(:database_document) do
12
+ setting :skip_authentication, false
13
+ setting :skip_authentication_allowlist, [], convert: ->(s) { s.split }
14
+ end
15
+
16
+ plugin :column_encryption do |enc|
17
+ enc.column :encryption_secret
18
+ end
19
+
20
+ def initialize(*)
21
+ super
22
+ self.encryption_secret ||= SecureRandom.hex(32)
23
+ end
24
+
25
+ def sign_url(path, expire_at:, params: {})
26
+ uri = URI(path)
27
+ q = params.merge(expire_at: expire_at.to_i)
28
+ uri.query = HTTParty::Request::NON_RAILS_QUERY_STRING_NORMALIZER.call(q)
29
+ url = uri.to_s
30
+ sig = self.digest_url(url)
31
+ return url + "&sig=#{sig}"
32
+ end
33
+
34
+ def check_url(url, now: Time.now)
35
+ sig_idx = url.rindex("&sig=")
36
+ return false if sig_idx.nil?
37
+ without_sig = url[...sig_idx]
38
+ got_sig = url[(sig_idx + 5)..]
39
+ real_sig = self.digest_url(without_sig)
40
+ return false unless ActiveSupport::SecurityUtils.secure_compare(got_sig, real_sig)
41
+ expires = CGI.parse(URI(url).query || "?")["expire_at"]
42
+ return false unless expires
43
+ t = Time.at(expires.first.to_i)
44
+ return false if t <= now
45
+ return true
46
+ end
47
+
48
+ protected def digest_url(url)
49
+ hmac = OpenSSL::HMAC.digest("sha256", self.encryption_secret, url)
50
+ b = Base64.urlsafe_encode64(hmac, padding: false)
51
+ return b
52
+ end
53
+
54
+ def presigned_view_url(expire_at:, **kw)
55
+ url = "#{Webhookdb.api_url}/admin/v1/database_documents/#{self.id}/view"
56
+ return self.sign_url(url, expire_at:, **kw)
57
+ end
58
+ end
59
+
60
+ # Table: database_documents
61
+ # --------------------------------------------------------------------------------------------
62
+ # Columns:
63
+ # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
64
+ # created_at | timestamp with time zone | NOT NULL DEFAULT now()
65
+ # key | text | NOT NULL
66
+ # content | bytea | NOT NULL
67
+ # content_type | text | NOT NULL
68
+ # encryption_secret | text | NOT NULL
69
+ # Indexes:
70
+ # database_documents_pkey | PRIMARY KEY btree (id)
71
+ # database_documents_key_key | UNIQUE btree (key)
72
+ # --------------------------------------------------------------------------------------------
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webhookdb::DBAdapter::ColumnTypes
4
+ BIGINT = :bigint
5
+ BIGINT_ARRAY = :bigintarray
6
+ BOOLEAN = :bool
7
+ DATE = :date
8
+ DECIMAL = :decimal
9
+ DOUBLE = :double
10
+ FLOAT = :float
11
+ INTEGER = :int
12
+ INTEGER_ARRAY = :intarray
13
+ TEXT_ARRAY = :textarray
14
+ OBJECT = :object
15
+ TEXT = :text
16
+ TIMESTAMP = :timestamp
17
+ UUID = :uuid
18
+
19
+ COLUMN_TYPES = Set.new(
20
+ [
21
+ BIGINT,
22
+ BIGINT_ARRAY,
23
+ BOOLEAN,
24
+ DATE,
25
+ DECIMAL,
26
+ DOUBLE,
27
+ FLOAT,
28
+ INTEGER,
29
+ INTEGER_ARRAY,
30
+ OBJECT,
31
+ TEXT,
32
+ TEXT_ARRAY,
33
+ TIMESTAMP,
34
+ UUID,
35
+ ],
36
+ )
37
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webhookdb::DBAdapter::DefaultSql
4
+ def create_schema_sql(schema, if_not_exists: false)
5
+ s = +"CREATE SCHEMA "
6
+ s << "IF NOT EXISTS " if if_not_exists
7
+ s << self.escape_identifier(schema.name)
8
+ return s
9
+ end
10
+
11
+ def create_table_sql(table, columns, if_not_exists: false)
12
+ createtable = +"CREATE TABLE "
13
+ createtable << "IF NOT EXISTS " if if_not_exists
14
+ createtable << self.qualify_table(table)
15
+ lines = ["#{createtable} ("]
16
+ columns[0...-1]&.each { |c| lines << " #{self.column_create_sql(c)}," }
17
+ lines << " #{self.column_create_sql(columns.last)}"
18
+ lines << ")"
19
+ return lines.join("\n")
20
+ end
21
+
22
+ def identifier_quote_char
23
+ raise NotImplementedError
24
+ end
25
+
26
+ # We write our own escaper because we want to only escape what's needed;
27
+ # otherwise we want to avoid quoting identifiers.
28
+ def escape_identifier(s)
29
+ s = s.to_s
30
+ raise ArgumentError, "#{s} is an invalid identifier and should have been validated previously" unless
31
+ Webhookdb::DBAdapter::VALID_IDENTIFIER.match?(s)
32
+
33
+ quo = self.identifier_quote_char
34
+ return "#{quo}#{s}#{quo}" if RESERVED_KEYWORDS.include?(s.upcase) ||
35
+ s.include?(" ") ||
36
+ s.include?("-") ||
37
+ s.start_with?(/\d/)
38
+ return s
39
+ end
40
+
41
+ # @param [Webhookdb::DBAdapter::Table] table
42
+ def qualify_table(table)
43
+ s = +""
44
+ if table.schema
45
+ s << self.escape_identifier(table.schema.name)
46
+ s << "."
47
+ end
48
+ s << self.escape_identifier(table.name)
49
+ return s
50
+ end
51
+
52
+ # Return the SQL string for column assignment.
53
+ # Like for src and dest of :src and :tgt,
54
+ # and columns with names :spam and :foo,
55
+ # return "tgt.spam = src.spam, tgt.foo = src.foo"
56
+ # Column names will be escaped; the source and destination values
57
+ # should already be valid identifiers (usually aliases for a table or query).
58
+ #
59
+ # If a block is given, call it with (column, left hand side string, right hand side string).
60
+ # It should return the new lhs/rhs strings.
61
+ #
62
+ # @param [String, nil] source Prefix (like table alias) for right hand side columns.
63
+ # @param [String, nil] destination nil Prefix (like table alias) for left hand side columns.
64
+ # @param [Array<Webhookdb::DBAdapter::Column>] columns
65
+ def assign_columns_sql(source, destination, columns, &block)
66
+ stmts = columns.map do |c|
67
+ cname = self.escape_identifier(c.name)
68
+ lhs = destination ? "#{destination}.#{cname}" : cname
69
+ rhs = source ? "#{source}.#{cname}" : cname
70
+ lhs, rhs = block[c, lhs, rhs] if block
71
+ "#{lhs} = #{rhs}"
72
+ end
73
+ return stmts.join(", ")
74
+ end
75
+
76
+ # These are all PG reserved keywords, as per https://www.postgresql.org/docs/current/sql-keywords-appendix.html
77
+ PG_RESERVED_KEYWORDS = Set.new(
78
+ [
79
+ "ALL",
80
+ "ANALYSE",
81
+ "ANALYZE",
82
+ "AND",
83
+ "ANY",
84
+ "ARRAY",
85
+ "AS",
86
+ "ASC",
87
+ "ASYMMETRIC",
88
+ "AUTHORIZATION",
89
+ "BINARY",
90
+ "BOTH",
91
+ "CASE",
92
+ "CAST",
93
+ "CHECK",
94
+ "COLLATE",
95
+ "COLLATION",
96
+ "COLUMN",
97
+ "CONCURRENTLY",
98
+ "CONSTRAINT",
99
+ "CREATE",
100
+ "CROSS",
101
+ "CURRENT_CATALOG",
102
+ "CURRENT_DATE",
103
+ "CURRENT_ROLE",
104
+ "CURRENT_SCHEMA",
105
+ "CURRENT_TIME",
106
+ "CURRENT_TIMESTAMP",
107
+ "CURRENT_USER",
108
+ "DECODE",
109
+ "DEFAULT",
110
+ "DEFERRABLE",
111
+ "DESC",
112
+ "DISTINCT",
113
+ "DISTRIBUTED",
114
+ "DO",
115
+ "ELSE",
116
+ "END",
117
+ "EXCEPT",
118
+ "FALSE",
119
+ "FETCH",
120
+ "FOR",
121
+ "FOREIGN",
122
+ "FREEZE",
123
+ "FROM",
124
+ "FULL",
125
+ "GRANT",
126
+ "GROUP",
127
+ "HAVING",
128
+ "ILIKE",
129
+ "IN",
130
+ "INITIALLY",
131
+ "INNER",
132
+ "INTERSECT",
133
+ "INTO",
134
+ "IS",
135
+ "ISNULL",
136
+ "JOIN",
137
+ "LATERAL",
138
+ "LEADING",
139
+ "LEFT",
140
+ "LIKE",
141
+ "LIMIT",
142
+ "LOCALTIME",
143
+ "LOCALTIMESTAMP",
144
+ "LOG",
145
+ "NATURAL",
146
+ "NOT",
147
+ "NOTNULL",
148
+ "NULL",
149
+ "OFFSET",
150
+ "ON",
151
+ "ONLY",
152
+ "OR",
153
+ "ORDER",
154
+ "OUTER",
155
+ "OVERLAPS",
156
+ "PLACING",
157
+ "PRIMARY",
158
+ "REFERENCES",
159
+ "RETURNING",
160
+ "RIGHT",
161
+ "SCATTER",
162
+ "SELECT",
163
+ "SESSION_USER",
164
+ "SIMILAR",
165
+ "SOME",
166
+ "SYMMETRIC",
167
+ "TABLE",
168
+ "THEN",
169
+ "TO",
170
+ "TRAILING",
171
+ "TRUE",
172
+ "UNION",
173
+ "UNIQUE",
174
+ "USER",
175
+ "USING",
176
+ "VARIADIC",
177
+ "VERBOSE",
178
+ "WHEN",
179
+ "WHERE",
180
+ "WINDOW",
181
+ "WITH",
182
+ ],
183
+ )
184
+
185
+ # Reserved keywords must be quoted to be used as identifiers.
186
+ RESERVED_KEYWORDS = PG_RESERVED_KEYWORDS
187
+ end