webhookdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (364) hide show
  1. checksums.yaml +7 -0
  2. data/data/messages/layouts/blank.email.liquid +10 -0
  3. data/data/messages/layouts/minimal.email.liquid +28 -0
  4. data/data/messages/layouts/standard.email.liquid +28 -0
  5. data/data/messages/partials/button.liquid +15 -0
  6. data/data/messages/partials/environment_banner.liquid +9 -0
  7. data/data/messages/partials/footer.liquid +22 -0
  8. data/data/messages/partials/greeting.liquid +3 -0
  9. data/data/messages/partials/logo_header.liquid +18 -0
  10. data/data/messages/partials/signoff.liquid +1 -0
  11. data/data/messages/styles/v1.liquid +346 -0
  12. data/data/messages/templates/errors/icalendar_fetch.email.liquid +29 -0
  13. data/data/messages/templates/invite.email.liquid +15 -0
  14. data/data/messages/templates/new_customer.email.liquid +24 -0
  15. data/data/messages/templates/org_database_migration_finished.email.liquid +7 -0
  16. data/data/messages/templates/org_database_migration_started.email.liquid +9 -0
  17. data/data/messages/templates/specs/_field_partial.liquid +1 -0
  18. data/data/messages/templates/specs/basic.email.liquid +2 -0
  19. data/data/messages/templates/specs/basic.fake.liquid +1 -0
  20. data/data/messages/templates/specs/with_field.email.liquid +2 -0
  21. data/data/messages/templates/specs/with_field.fake.liquid +1 -0
  22. data/data/messages/templates/specs/with_include.email.liquid +2 -0
  23. data/data/messages/templates/specs/with_partial.email.liquid +1 -0
  24. data/data/messages/templates/verification.email.liquid +14 -0
  25. data/data/messages/templates/verification.sms.liquid +1 -0
  26. data/data/messages/web/install-customer-login.liquid +48 -0
  27. data/data/messages/web/install-error.liquid +17 -0
  28. data/data/messages/web/install-success.liquid +35 -0
  29. data/data/messages/web/install.liquid +20 -0
  30. data/data/messages/web/partials/footer.liquid +4 -0
  31. data/data/messages/web/partials/form_error.liquid +1 -0
  32. data/data/messages/web/partials/header.liquid +3 -0
  33. data/data/messages/web/styles.liquid +134 -0
  34. data/data/windows_tz.txt +461 -0
  35. data/db/migrations/001_testing_pixies.rb +13 -0
  36. data/db/migrations/002_initial.rb +132 -0
  37. data/db/migrations/003_ux_overhaul.rb +20 -0
  38. data/db/migrations/004_incremental_backfill.rb +9 -0
  39. data/db/migrations/005_log_webhooks.rb +24 -0
  40. data/db/migrations/006_generalize_roles.rb +29 -0
  41. data/db/migrations/007_org_dns.rb +12 -0
  42. data/db/migrations/008_webhook_subscriptions.rb +19 -0
  43. data/db/migrations/009_nonunique_stripe_subscription_customer.rb +16 -0
  44. data/db/migrations/010_drop_integration_soft_delete.rb +14 -0
  45. data/db/migrations/011_webhook_subscriptions_created_at.rb +10 -0
  46. data/db/migrations/012_webhook_subscriptions_created_by.rb +9 -0
  47. data/db/migrations/013_default_org_membership.rb +30 -0
  48. data/db/migrations/014_webhook_subscription_deliveries.rb +26 -0
  49. data/db/migrations/015_dependent_integrations.rb +9 -0
  50. data/db/migrations/016_encrypted_columns.rb +9 -0
  51. data/db/migrations/017_skip_verification.rb +9 -0
  52. data/db/migrations/018_sync_targets.rb +25 -0
  53. data/db/migrations/019_org_schema.rb +9 -0
  54. data/db/migrations/020_org_database_migrations.rb +25 -0
  55. data/db/migrations/021_no_default_org_schema.rb +14 -0
  56. data/db/migrations/022_database_document.rb +15 -0
  57. data/db/migrations/023_sync_target_schema.rb +9 -0
  58. data/db/migrations/024_org_semaphore_jobs.rb +9 -0
  59. data/db/migrations/025_integration_backfill_cursor.rb +9 -0
  60. data/db/migrations/026_undo_integration_backfill_cursor.rb +9 -0
  61. data/db/migrations/027_sync_target_http_sync.rb +12 -0
  62. data/db/migrations/028_logged_webhook_path.rb +24 -0
  63. data/db/migrations/029_encrypt_columns.rb +97 -0
  64. data/db/migrations/030_org_sync_target_timeout.rb +9 -0
  65. data/db/migrations/031_org_max_query_rows.rb +9 -0
  66. data/db/migrations/032_remove_db_defaults.rb +12 -0
  67. data/db/migrations/033_backfill_jobs.rb +26 -0
  68. data/db/migrations/034_backfill_job_criteria.rb +9 -0
  69. data/db/migrations/035_synchronous_backfill.rb +9 -0
  70. data/db/migrations/036_oauth.rb +26 -0
  71. data/db/migrations/037_oauth_used.rb +9 -0
  72. data/lib/amigo/durable_job.rb +416 -0
  73. data/lib/pry/clipboard.rb +111 -0
  74. data/lib/sequel/advisory_lock.rb +65 -0
  75. data/lib/webhookdb/admin.rb +4 -0
  76. data/lib/webhookdb/admin_api/auth.rb +36 -0
  77. data/lib/webhookdb/admin_api/customers.rb +63 -0
  78. data/lib/webhookdb/admin_api/database_documents.rb +20 -0
  79. data/lib/webhookdb/admin_api/entities.rb +66 -0
  80. data/lib/webhookdb/admin_api/message_deliveries.rb +61 -0
  81. data/lib/webhookdb/admin_api/roles.rb +15 -0
  82. data/lib/webhookdb/admin_api.rb +34 -0
  83. data/lib/webhookdb/aggregate_result.rb +63 -0
  84. data/lib/webhookdb/api/auth.rb +122 -0
  85. data/lib/webhookdb/api/connstr_auth.rb +36 -0
  86. data/lib/webhookdb/api/db.rb +188 -0
  87. data/lib/webhookdb/api/demo.rb +14 -0
  88. data/lib/webhookdb/api/entities.rb +198 -0
  89. data/lib/webhookdb/api/helpers.rb +253 -0
  90. data/lib/webhookdb/api/install.rb +296 -0
  91. data/lib/webhookdb/api/me.rb +53 -0
  92. data/lib/webhookdb/api/organizations.rb +254 -0
  93. data/lib/webhookdb/api/replay.rb +64 -0
  94. data/lib/webhookdb/api/service_integrations.rb +402 -0
  95. data/lib/webhookdb/api/services.rb +27 -0
  96. data/lib/webhookdb/api/stripe.rb +22 -0
  97. data/lib/webhookdb/api/subscriptions.rb +67 -0
  98. data/lib/webhookdb/api/sync_targets.rb +232 -0
  99. data/lib/webhookdb/api/system.rb +37 -0
  100. data/lib/webhookdb/api/webhook_subscriptions.rb +96 -0
  101. data/lib/webhookdb/api.rb +92 -0
  102. data/lib/webhookdb/apps.rb +93 -0
  103. data/lib/webhookdb/async/audit_logger.rb +38 -0
  104. data/lib/webhookdb/async/autoscaler.rb +84 -0
  105. data/lib/webhookdb/async/job.rb +18 -0
  106. data/lib/webhookdb/async/job_logger.rb +45 -0
  107. data/lib/webhookdb/async/scheduled_job.rb +18 -0
  108. data/lib/webhookdb/async.rb +142 -0
  109. data/lib/webhookdb/aws.rb +98 -0
  110. data/lib/webhookdb/backfill_job.rb +107 -0
  111. data/lib/webhookdb/backfiller.rb +107 -0
  112. data/lib/webhookdb/cloudflare.rb +39 -0
  113. data/lib/webhookdb/connection_cache.rb +177 -0
  114. data/lib/webhookdb/console.rb +71 -0
  115. data/lib/webhookdb/convertkit.rb +14 -0
  116. data/lib/webhookdb/crypto.rb +66 -0
  117. data/lib/webhookdb/customer/reset_code.rb +94 -0
  118. data/lib/webhookdb/customer.rb +347 -0
  119. data/lib/webhookdb/database_document.rb +72 -0
  120. data/lib/webhookdb/db_adapter/column_types.rb +37 -0
  121. data/lib/webhookdb/db_adapter/default_sql.rb +187 -0
  122. data/lib/webhookdb/db_adapter/pg.rb +96 -0
  123. data/lib/webhookdb/db_adapter/snowflake.rb +137 -0
  124. data/lib/webhookdb/db_adapter.rb +208 -0
  125. data/lib/webhookdb/dbutil.rb +92 -0
  126. data/lib/webhookdb/demo_mode.rb +100 -0
  127. data/lib/webhookdb/developer_alert.rb +51 -0
  128. data/lib/webhookdb/email_octopus.rb +21 -0
  129. data/lib/webhookdb/enumerable.rb +18 -0
  130. data/lib/webhookdb/fixtures/backfill_jobs.rb +72 -0
  131. data/lib/webhookdb/fixtures/customers.rb +65 -0
  132. data/lib/webhookdb/fixtures/database_documents.rb +27 -0
  133. data/lib/webhookdb/fixtures/faker.rb +41 -0
  134. data/lib/webhookdb/fixtures/logged_webhooks.rb +56 -0
  135. data/lib/webhookdb/fixtures/message_deliveries.rb +59 -0
  136. data/lib/webhookdb/fixtures/oauth_sessions.rb +24 -0
  137. data/lib/webhookdb/fixtures/organization_database_migrations.rb +37 -0
  138. data/lib/webhookdb/fixtures/organization_memberships.rb +54 -0
  139. data/lib/webhookdb/fixtures/organizations.rb +32 -0
  140. data/lib/webhookdb/fixtures/reset_codes.rb +23 -0
  141. data/lib/webhookdb/fixtures/service_integrations.rb +42 -0
  142. data/lib/webhookdb/fixtures/subscriptions.rb +33 -0
  143. data/lib/webhookdb/fixtures/sync_targets.rb +32 -0
  144. data/lib/webhookdb/fixtures/webhook_subscriptions.rb +35 -0
  145. data/lib/webhookdb/fixtures.rb +15 -0
  146. data/lib/webhookdb/formatting.rb +56 -0
  147. data/lib/webhookdb/front.rb +49 -0
  148. data/lib/webhookdb/github.rb +22 -0
  149. data/lib/webhookdb/google_calendar.rb +29 -0
  150. data/lib/webhookdb/heroku.rb +21 -0
  151. data/lib/webhookdb/http.rb +114 -0
  152. data/lib/webhookdb/icalendar.rb +17 -0
  153. data/lib/webhookdb/id.rb +17 -0
  154. data/lib/webhookdb/idempotency.rb +90 -0
  155. data/lib/webhookdb/increase.rb +42 -0
  156. data/lib/webhookdb/intercom.rb +23 -0
  157. data/lib/webhookdb/jobs/amigo_test_jobs.rb +118 -0
  158. data/lib/webhookdb/jobs/backfill.rb +32 -0
  159. data/lib/webhookdb/jobs/create_mirror_table.rb +18 -0
  160. data/lib/webhookdb/jobs/create_stripe_customer.rb +17 -0
  161. data/lib/webhookdb/jobs/customer_created_notify_internal.rb +22 -0
  162. data/lib/webhookdb/jobs/demo_mode_sync_data.rb +19 -0
  163. data/lib/webhookdb/jobs/deprecated_jobs.rb +19 -0
  164. data/lib/webhookdb/jobs/developer_alert_handle.rb +14 -0
  165. data/lib/webhookdb/jobs/durable_job_recheck_poller.rb +17 -0
  166. data/lib/webhookdb/jobs/emailer.rb +15 -0
  167. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +25 -0
  168. data/lib/webhookdb/jobs/icalendar_sync.rb +23 -0
  169. data/lib/webhookdb/jobs/logged_webhook_replay.rb +17 -0
  170. data/lib/webhookdb/jobs/logged_webhook_resilient_replay.rb +15 -0
  171. data/lib/webhookdb/jobs/message_dispatched.rb +16 -0
  172. data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +21 -0
  173. data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +21 -0
  174. data/lib/webhookdb/jobs/organization_database_migration_run.rb +24 -0
  175. data/lib/webhookdb/jobs/prepare_database_connections.rb +22 -0
  176. data/lib/webhookdb/jobs/process_webhook.rb +47 -0
  177. data/lib/webhookdb/jobs/renew_watch_channel.rb +24 -0
  178. data/lib/webhookdb/jobs/replication_migration.rb +24 -0
  179. data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +23 -0
  180. data/lib/webhookdb/jobs/scheduled_backfills.rb +77 -0
  181. data/lib/webhookdb/jobs/send_invite.rb +15 -0
  182. data/lib/webhookdb/jobs/send_test_webhook.rb +25 -0
  183. data/lib/webhookdb/jobs/send_webhook.rb +20 -0
  184. data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +16 -0
  185. data/lib/webhookdb/jobs/sync_target_run_sync.rb +38 -0
  186. data/lib/webhookdb/jobs/trim_logged_webhooks.rb +15 -0
  187. data/lib/webhookdb/jobs/webhook_resource_notify_integrations.rb +30 -0
  188. data/lib/webhookdb/jobs/webhook_subscription_delivery_attempt.rb +29 -0
  189. data/lib/webhookdb/jobs.rb +4 -0
  190. data/lib/webhookdb/json.rb +113 -0
  191. data/lib/webhookdb/liquid/expose.rb +27 -0
  192. data/lib/webhookdb/liquid/filters.rb +16 -0
  193. data/lib/webhookdb/liquid/liquification.rb +26 -0
  194. data/lib/webhookdb/liquid/partial.rb +12 -0
  195. data/lib/webhookdb/logged_webhook/resilient.rb +95 -0
  196. data/lib/webhookdb/logged_webhook.rb +194 -0
  197. data/lib/webhookdb/message/body.rb +25 -0
  198. data/lib/webhookdb/message/delivery.rb +127 -0
  199. data/lib/webhookdb/message/email_transport.rb +133 -0
  200. data/lib/webhookdb/message/fake_transport.rb +54 -0
  201. data/lib/webhookdb/message/liquid_drops.rb +29 -0
  202. data/lib/webhookdb/message/template.rb +89 -0
  203. data/lib/webhookdb/message/transport.rb +43 -0
  204. data/lib/webhookdb/message.rb +150 -0
  205. data/lib/webhookdb/messages/error_icalendar_fetch.rb +42 -0
  206. data/lib/webhookdb/messages/invite.rb +23 -0
  207. data/lib/webhookdb/messages/new_customer.rb +14 -0
  208. data/lib/webhookdb/messages/org_database_migration_finished.rb +23 -0
  209. data/lib/webhookdb/messages/org_database_migration_started.rb +24 -0
  210. data/lib/webhookdb/messages/specs.rb +57 -0
  211. data/lib/webhookdb/messages/verification.rb +23 -0
  212. data/lib/webhookdb/method_utilities.rb +82 -0
  213. data/lib/webhookdb/microsoft_calendar.rb +36 -0
  214. data/lib/webhookdb/nextpax.rb +14 -0
  215. data/lib/webhookdb/oauth/front.rb +58 -0
  216. data/lib/webhookdb/oauth/intercom.rb +58 -0
  217. data/lib/webhookdb/oauth/session.rb +24 -0
  218. data/lib/webhookdb/oauth.rb +80 -0
  219. data/lib/webhookdb/organization/alerting.rb +35 -0
  220. data/lib/webhookdb/organization/database_migration.rb +151 -0
  221. data/lib/webhookdb/organization/db_builder.rb +429 -0
  222. data/lib/webhookdb/organization.rb +506 -0
  223. data/lib/webhookdb/organization_membership.rb +58 -0
  224. data/lib/webhookdb/phone_number.rb +38 -0
  225. data/lib/webhookdb/plaid.rb +23 -0
  226. data/lib/webhookdb/platform.rb +27 -0
  227. data/lib/webhookdb/plivo.rb +52 -0
  228. data/lib/webhookdb/postgres/maintenance.rb +166 -0
  229. data/lib/webhookdb/postgres/model.rb +82 -0
  230. data/lib/webhookdb/postgres/model_utilities.rb +382 -0
  231. data/lib/webhookdb/postgres/testing_pixie.rb +16 -0
  232. data/lib/webhookdb/postgres/validations.rb +46 -0
  233. data/lib/webhookdb/postgres.rb +176 -0
  234. data/lib/webhookdb/postmark.rb +20 -0
  235. data/lib/webhookdb/redis.rb +35 -0
  236. data/lib/webhookdb/replicator/atom_single_feed_v1.rb +116 -0
  237. data/lib/webhookdb/replicator/aws_pricing_v1.rb +488 -0
  238. data/lib/webhookdb/replicator/base.rb +1185 -0
  239. data/lib/webhookdb/replicator/column.rb +482 -0
  240. data/lib/webhookdb/replicator/convertkit_broadcast_v1.rb +69 -0
  241. data/lib/webhookdb/replicator/convertkit_subscriber_v1.rb +200 -0
  242. data/lib/webhookdb/replicator/convertkit_tag_v1.rb +66 -0
  243. data/lib/webhookdb/replicator/convertkit_v1_mixin.rb +65 -0
  244. data/lib/webhookdb/replicator/docgen.rb +167 -0
  245. data/lib/webhookdb/replicator/email_octopus_campaign_v1.rb +84 -0
  246. data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +159 -0
  247. data/lib/webhookdb/replicator/email_octopus_event_v1.rb +244 -0
  248. data/lib/webhookdb/replicator/email_octopus_list_v1.rb +101 -0
  249. data/lib/webhookdb/replicator/fake.rb +453 -0
  250. data/lib/webhookdb/replicator/front_conversation_v1.rb +45 -0
  251. data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +55 -0
  252. data/lib/webhookdb/replicator/front_message_v1.rb +45 -0
  253. data/lib/webhookdb/replicator/front_v1_mixin.rb +22 -0
  254. data/lib/webhookdb/replicator/github_issue_comment_v1.rb +58 -0
  255. data/lib/webhookdb/replicator/github_issue_v1.rb +83 -0
  256. data/lib/webhookdb/replicator/github_pull_v1.rb +84 -0
  257. data/lib/webhookdb/replicator/github_release_v1.rb +47 -0
  258. data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +250 -0
  259. data/lib/webhookdb/replicator/github_repository_event_v1.rb +45 -0
  260. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +465 -0
  261. data/lib/webhookdb/replicator/icalendar_event_v1.rb +334 -0
  262. data/lib/webhookdb/replicator/increase_account_number_v1.rb +77 -0
  263. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +61 -0
  264. data/lib/webhookdb/replicator/increase_account_v1.rb +63 -0
  265. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +78 -0
  266. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +64 -0
  267. data/lib/webhookdb/replicator/increase_limit_v1.rb +78 -0
  268. data/lib/webhookdb/replicator/increase_transaction_v1.rb +74 -0
  269. data/lib/webhookdb/replicator/increase_v1_mixin.rb +121 -0
  270. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +61 -0
  271. data/lib/webhookdb/replicator/intercom_contact_v1.rb +36 -0
  272. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +38 -0
  273. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +69 -0
  274. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +105 -0
  275. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +65 -0
  276. data/lib/webhookdb/replicator/plivo_sms_inbound_v1.rb +102 -0
  277. data/lib/webhookdb/replicator/postmark_inbound_message_v1.rb +94 -0
  278. data/lib/webhookdb/replicator/postmark_outbound_message_event_v1.rb +107 -0
  279. data/lib/webhookdb/replicator/schema_modification.rb +42 -0
  280. data/lib/webhookdb/replicator/shopify_customer_v1.rb +58 -0
  281. data/lib/webhookdb/replicator/shopify_order_v1.rb +64 -0
  282. data/lib/webhookdb/replicator/shopify_v1_mixin.rb +161 -0
  283. data/lib/webhookdb/replicator/signalwire_message_v1.rb +169 -0
  284. data/lib/webhookdb/replicator/sponsy_customer_v1.rb +54 -0
  285. data/lib/webhookdb/replicator/sponsy_placement_v1.rb +34 -0
  286. data/lib/webhookdb/replicator/sponsy_publication_v1.rb +125 -0
  287. data/lib/webhookdb/replicator/sponsy_slot_v1.rb +41 -0
  288. data/lib/webhookdb/replicator/sponsy_status_v1.rb +35 -0
  289. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +165 -0
  290. data/lib/webhookdb/replicator/state_machine_step.rb +69 -0
  291. data/lib/webhookdb/replicator/stripe_charge_v1.rb +77 -0
  292. data/lib/webhookdb/replicator/stripe_coupon_v1.rb +62 -0
  293. data/lib/webhookdb/replicator/stripe_customer_v1.rb +60 -0
  294. data/lib/webhookdb/replicator/stripe_dispute_v1.rb +77 -0
  295. data/lib/webhookdb/replicator/stripe_invoice_item_v1.rb +82 -0
  296. data/lib/webhookdb/replicator/stripe_invoice_v1.rb +116 -0
  297. data/lib/webhookdb/replicator/stripe_payout_v1.rb +67 -0
  298. data/lib/webhookdb/replicator/stripe_price_v1.rb +60 -0
  299. data/lib/webhookdb/replicator/stripe_product_v1.rb +60 -0
  300. data/lib/webhookdb/replicator/stripe_refund_v1.rb +101 -0
  301. data/lib/webhookdb/replicator/stripe_subscription_item_v1.rb +56 -0
  302. data/lib/webhookdb/replicator/stripe_subscription_v1.rb +75 -0
  303. data/lib/webhookdb/replicator/stripe_v1_mixin.rb +116 -0
  304. data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +141 -0
  305. data/lib/webhookdb/replicator/transistor_episode_v1.rb +169 -0
  306. data/lib/webhookdb/replicator/transistor_show_v1.rb +68 -0
  307. data/lib/webhookdb/replicator/transistor_v1_mixin.rb +65 -0
  308. data/lib/webhookdb/replicator/twilio_sms_v1.rb +156 -0
  309. data/lib/webhookdb/replicator/webhook_request.rb +5 -0
  310. data/lib/webhookdb/replicator/webhookdb_customer_v1.rb +74 -0
  311. data/lib/webhookdb/replicator.rb +224 -0
  312. data/lib/webhookdb/role.rb +42 -0
  313. data/lib/webhookdb/sentry.rb +35 -0
  314. data/lib/webhookdb/service/auth.rb +138 -0
  315. data/lib/webhookdb/service/collection.rb +91 -0
  316. data/lib/webhookdb/service/entities.rb +97 -0
  317. data/lib/webhookdb/service/helpers.rb +270 -0
  318. data/lib/webhookdb/service/middleware.rb +124 -0
  319. data/lib/webhookdb/service/types.rb +30 -0
  320. data/lib/webhookdb/service/validators.rb +32 -0
  321. data/lib/webhookdb/service/view_api.rb +63 -0
  322. data/lib/webhookdb/service.rb +219 -0
  323. data/lib/webhookdb/service_integration.rb +332 -0
  324. data/lib/webhookdb/shopify.rb +35 -0
  325. data/lib/webhookdb/signalwire.rb +13 -0
  326. data/lib/webhookdb/slack.rb +68 -0
  327. data/lib/webhookdb/snowflake.rb +90 -0
  328. data/lib/webhookdb/spec_helpers/async.rb +122 -0
  329. data/lib/webhookdb/spec_helpers/citest.rb +88 -0
  330. data/lib/webhookdb/spec_helpers/integration.rb +121 -0
  331. data/lib/webhookdb/spec_helpers/message.rb +41 -0
  332. data/lib/webhookdb/spec_helpers/postgres.rb +220 -0
  333. data/lib/webhookdb/spec_helpers/service.rb +432 -0
  334. data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +56 -0
  335. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +915 -0
  336. data/lib/webhookdb/spec_helpers/whdb.rb +139 -0
  337. data/lib/webhookdb/spec_helpers.rb +63 -0
  338. data/lib/webhookdb/sponsy.rb +14 -0
  339. data/lib/webhookdb/stripe.rb +37 -0
  340. data/lib/webhookdb/subscription.rb +203 -0
  341. data/lib/webhookdb/sync_target.rb +491 -0
  342. data/lib/webhookdb/tasks/admin.rb +49 -0
  343. data/lib/webhookdb/tasks/annotate.rb +36 -0
  344. data/lib/webhookdb/tasks/db.rb +82 -0
  345. data/lib/webhookdb/tasks/docs.rb +42 -0
  346. data/lib/webhookdb/tasks/fixture.rb +35 -0
  347. data/lib/webhookdb/tasks/message.rb +50 -0
  348. data/lib/webhookdb/tasks/regress.rb +87 -0
  349. data/lib/webhookdb/tasks/release.rb +27 -0
  350. data/lib/webhookdb/tasks/sidekiq.rb +23 -0
  351. data/lib/webhookdb/tasks/specs.rb +64 -0
  352. data/lib/webhookdb/theranest.rb +15 -0
  353. data/lib/webhookdb/transistor.rb +13 -0
  354. data/lib/webhookdb/twilio.rb +13 -0
  355. data/lib/webhookdb/typed_struct.rb +44 -0
  356. data/lib/webhookdb/version.rb +5 -0
  357. data/lib/webhookdb/webhook_response.rb +50 -0
  358. data/lib/webhookdb/webhook_subscription/delivery.rb +82 -0
  359. data/lib/webhookdb/webhook_subscription.rb +226 -0
  360. data/lib/webhookdb/windows_tz.rb +32 -0
  361. data/lib/webhookdb/xml.rb +92 -0
  362. data/lib/webhookdb.rb +224 -0
  363. data/lib/webterm/apps.rb +45 -0
  364. metadata +1129 -0
@@ -0,0 +1,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