webhookdb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/data/messages/layouts/blank.email.liquid +10 -0
- data/data/messages/layouts/minimal.email.liquid +28 -0
- data/data/messages/layouts/standard.email.liquid +28 -0
- data/data/messages/partials/button.liquid +15 -0
- data/data/messages/partials/environment_banner.liquid +9 -0
- data/data/messages/partials/footer.liquid +22 -0
- data/data/messages/partials/greeting.liquid +3 -0
- data/data/messages/partials/logo_header.liquid +18 -0
- data/data/messages/partials/signoff.liquid +1 -0
- data/data/messages/styles/v1.liquid +346 -0
- data/data/messages/templates/errors/icalendar_fetch.email.liquid +29 -0
- data/data/messages/templates/invite.email.liquid +15 -0
- data/data/messages/templates/new_customer.email.liquid +24 -0
- data/data/messages/templates/org_database_migration_finished.email.liquid +7 -0
- data/data/messages/templates/org_database_migration_started.email.liquid +9 -0
- data/data/messages/templates/specs/_field_partial.liquid +1 -0
- data/data/messages/templates/specs/basic.email.liquid +2 -0
- data/data/messages/templates/specs/basic.fake.liquid +1 -0
- data/data/messages/templates/specs/with_field.email.liquid +2 -0
- data/data/messages/templates/specs/with_field.fake.liquid +1 -0
- data/data/messages/templates/specs/with_include.email.liquid +2 -0
- data/data/messages/templates/specs/with_partial.email.liquid +1 -0
- data/data/messages/templates/verification.email.liquid +14 -0
- data/data/messages/templates/verification.sms.liquid +1 -0
- data/data/messages/web/install-customer-login.liquid +48 -0
- data/data/messages/web/install-error.liquid +17 -0
- data/data/messages/web/install-success.liquid +35 -0
- data/data/messages/web/install.liquid +20 -0
- data/data/messages/web/partials/footer.liquid +4 -0
- data/data/messages/web/partials/form_error.liquid +1 -0
- data/data/messages/web/partials/header.liquid +3 -0
- data/data/messages/web/styles.liquid +134 -0
- data/data/windows_tz.txt +461 -0
- data/db/migrations/001_testing_pixies.rb +13 -0
- data/db/migrations/002_initial.rb +132 -0
- data/db/migrations/003_ux_overhaul.rb +20 -0
- data/db/migrations/004_incremental_backfill.rb +9 -0
- data/db/migrations/005_log_webhooks.rb +24 -0
- data/db/migrations/006_generalize_roles.rb +29 -0
- data/db/migrations/007_org_dns.rb +12 -0
- data/db/migrations/008_webhook_subscriptions.rb +19 -0
- data/db/migrations/009_nonunique_stripe_subscription_customer.rb +16 -0
- data/db/migrations/010_drop_integration_soft_delete.rb +14 -0
- data/db/migrations/011_webhook_subscriptions_created_at.rb +10 -0
- data/db/migrations/012_webhook_subscriptions_created_by.rb +9 -0
- data/db/migrations/013_default_org_membership.rb +30 -0
- data/db/migrations/014_webhook_subscription_deliveries.rb +26 -0
- data/db/migrations/015_dependent_integrations.rb +9 -0
- data/db/migrations/016_encrypted_columns.rb +9 -0
- data/db/migrations/017_skip_verification.rb +9 -0
- data/db/migrations/018_sync_targets.rb +25 -0
- data/db/migrations/019_org_schema.rb +9 -0
- data/db/migrations/020_org_database_migrations.rb +25 -0
- data/db/migrations/021_no_default_org_schema.rb +14 -0
- data/db/migrations/022_database_document.rb +15 -0
- data/db/migrations/023_sync_target_schema.rb +9 -0
- data/db/migrations/024_org_semaphore_jobs.rb +9 -0
- data/db/migrations/025_integration_backfill_cursor.rb +9 -0
- data/db/migrations/026_undo_integration_backfill_cursor.rb +9 -0
- data/db/migrations/027_sync_target_http_sync.rb +12 -0
- data/db/migrations/028_logged_webhook_path.rb +24 -0
- data/db/migrations/029_encrypt_columns.rb +97 -0
- data/db/migrations/030_org_sync_target_timeout.rb +9 -0
- data/db/migrations/031_org_max_query_rows.rb +9 -0
- data/db/migrations/032_remove_db_defaults.rb +12 -0
- data/db/migrations/033_backfill_jobs.rb +26 -0
- data/db/migrations/034_backfill_job_criteria.rb +9 -0
- data/db/migrations/035_synchronous_backfill.rb +9 -0
- data/db/migrations/036_oauth.rb +26 -0
- data/db/migrations/037_oauth_used.rb +9 -0
- data/lib/amigo/durable_job.rb +416 -0
- data/lib/pry/clipboard.rb +111 -0
- data/lib/sequel/advisory_lock.rb +65 -0
- data/lib/webhookdb/admin.rb +4 -0
- data/lib/webhookdb/admin_api/auth.rb +36 -0
- data/lib/webhookdb/admin_api/customers.rb +63 -0
- data/lib/webhookdb/admin_api/database_documents.rb +20 -0
- data/lib/webhookdb/admin_api/entities.rb +66 -0
- data/lib/webhookdb/admin_api/message_deliveries.rb +61 -0
- data/lib/webhookdb/admin_api/roles.rb +15 -0
- data/lib/webhookdb/admin_api.rb +34 -0
- data/lib/webhookdb/aggregate_result.rb +63 -0
- data/lib/webhookdb/api/auth.rb +122 -0
- data/lib/webhookdb/api/connstr_auth.rb +36 -0
- data/lib/webhookdb/api/db.rb +188 -0
- data/lib/webhookdb/api/demo.rb +14 -0
- data/lib/webhookdb/api/entities.rb +198 -0
- data/lib/webhookdb/api/helpers.rb +253 -0
- data/lib/webhookdb/api/install.rb +296 -0
- data/lib/webhookdb/api/me.rb +53 -0
- data/lib/webhookdb/api/organizations.rb +254 -0
- data/lib/webhookdb/api/replay.rb +64 -0
- data/lib/webhookdb/api/service_integrations.rb +402 -0
- data/lib/webhookdb/api/services.rb +27 -0
- data/lib/webhookdb/api/stripe.rb +22 -0
- data/lib/webhookdb/api/subscriptions.rb +67 -0
- data/lib/webhookdb/api/sync_targets.rb +232 -0
- data/lib/webhookdb/api/system.rb +37 -0
- data/lib/webhookdb/api/webhook_subscriptions.rb +96 -0
- data/lib/webhookdb/api.rb +92 -0
- data/lib/webhookdb/apps.rb +93 -0
- data/lib/webhookdb/async/audit_logger.rb +38 -0
- data/lib/webhookdb/async/autoscaler.rb +84 -0
- data/lib/webhookdb/async/job.rb +18 -0
- data/lib/webhookdb/async/job_logger.rb +45 -0
- data/lib/webhookdb/async/scheduled_job.rb +18 -0
- data/lib/webhookdb/async.rb +142 -0
- data/lib/webhookdb/aws.rb +98 -0
- data/lib/webhookdb/backfill_job.rb +107 -0
- data/lib/webhookdb/backfiller.rb +107 -0
- data/lib/webhookdb/cloudflare.rb +39 -0
- data/lib/webhookdb/connection_cache.rb +177 -0
- data/lib/webhookdb/console.rb +71 -0
- data/lib/webhookdb/convertkit.rb +14 -0
- data/lib/webhookdb/crypto.rb +66 -0
- data/lib/webhookdb/customer/reset_code.rb +94 -0
- data/lib/webhookdb/customer.rb +347 -0
- data/lib/webhookdb/database_document.rb +72 -0
- data/lib/webhookdb/db_adapter/column_types.rb +37 -0
- data/lib/webhookdb/db_adapter/default_sql.rb +187 -0
- data/lib/webhookdb/db_adapter/pg.rb +96 -0
- data/lib/webhookdb/db_adapter/snowflake.rb +137 -0
- data/lib/webhookdb/db_adapter.rb +208 -0
- data/lib/webhookdb/dbutil.rb +92 -0
- data/lib/webhookdb/demo_mode.rb +100 -0
- data/lib/webhookdb/developer_alert.rb +51 -0
- data/lib/webhookdb/email_octopus.rb +21 -0
- data/lib/webhookdb/enumerable.rb +18 -0
- data/lib/webhookdb/fixtures/backfill_jobs.rb +72 -0
- data/lib/webhookdb/fixtures/customers.rb +65 -0
- data/lib/webhookdb/fixtures/database_documents.rb +27 -0
- data/lib/webhookdb/fixtures/faker.rb +41 -0
- data/lib/webhookdb/fixtures/logged_webhooks.rb +56 -0
- data/lib/webhookdb/fixtures/message_deliveries.rb +59 -0
- data/lib/webhookdb/fixtures/oauth_sessions.rb +24 -0
- data/lib/webhookdb/fixtures/organization_database_migrations.rb +37 -0
- data/lib/webhookdb/fixtures/organization_memberships.rb +54 -0
- data/lib/webhookdb/fixtures/organizations.rb +32 -0
- data/lib/webhookdb/fixtures/reset_codes.rb +23 -0
- data/lib/webhookdb/fixtures/service_integrations.rb +42 -0
- data/lib/webhookdb/fixtures/subscriptions.rb +33 -0
- data/lib/webhookdb/fixtures/sync_targets.rb +32 -0
- data/lib/webhookdb/fixtures/webhook_subscriptions.rb +35 -0
- data/lib/webhookdb/fixtures.rb +15 -0
- data/lib/webhookdb/formatting.rb +56 -0
- data/lib/webhookdb/front.rb +49 -0
- data/lib/webhookdb/github.rb +22 -0
- data/lib/webhookdb/google_calendar.rb +29 -0
- data/lib/webhookdb/heroku.rb +21 -0
- data/lib/webhookdb/http.rb +114 -0
- data/lib/webhookdb/icalendar.rb +17 -0
- data/lib/webhookdb/id.rb +17 -0
- data/lib/webhookdb/idempotency.rb +90 -0
- data/lib/webhookdb/increase.rb +42 -0
- data/lib/webhookdb/intercom.rb +23 -0
- data/lib/webhookdb/jobs/amigo_test_jobs.rb +118 -0
- data/lib/webhookdb/jobs/backfill.rb +32 -0
- data/lib/webhookdb/jobs/create_mirror_table.rb +18 -0
- data/lib/webhookdb/jobs/create_stripe_customer.rb +17 -0
- data/lib/webhookdb/jobs/customer_created_notify_internal.rb +22 -0
- data/lib/webhookdb/jobs/demo_mode_sync_data.rb +19 -0
- data/lib/webhookdb/jobs/deprecated_jobs.rb +19 -0
- data/lib/webhookdb/jobs/developer_alert_handle.rb +14 -0
- data/lib/webhookdb/jobs/durable_job_recheck_poller.rb +17 -0
- data/lib/webhookdb/jobs/emailer.rb +15 -0
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +25 -0
- data/lib/webhookdb/jobs/icalendar_sync.rb +23 -0
- data/lib/webhookdb/jobs/logged_webhook_replay.rb +17 -0
- data/lib/webhookdb/jobs/logged_webhook_resilient_replay.rb +15 -0
- data/lib/webhookdb/jobs/message_dispatched.rb +16 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +21 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +21 -0
- data/lib/webhookdb/jobs/organization_database_migration_run.rb +24 -0
- data/lib/webhookdb/jobs/prepare_database_connections.rb +22 -0
- data/lib/webhookdb/jobs/process_webhook.rb +47 -0
- data/lib/webhookdb/jobs/renew_watch_channel.rb +24 -0
- data/lib/webhookdb/jobs/replication_migration.rb +24 -0
- data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +23 -0
- data/lib/webhookdb/jobs/scheduled_backfills.rb +77 -0
- data/lib/webhookdb/jobs/send_invite.rb +15 -0
- data/lib/webhookdb/jobs/send_test_webhook.rb +25 -0
- data/lib/webhookdb/jobs/send_webhook.rb +20 -0
- data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +16 -0
- data/lib/webhookdb/jobs/sync_target_run_sync.rb +38 -0
- data/lib/webhookdb/jobs/trim_logged_webhooks.rb +15 -0
- data/lib/webhookdb/jobs/webhook_resource_notify_integrations.rb +30 -0
- data/lib/webhookdb/jobs/webhook_subscription_delivery_attempt.rb +29 -0
- data/lib/webhookdb/jobs.rb +4 -0
- data/lib/webhookdb/json.rb +113 -0
- data/lib/webhookdb/liquid/expose.rb +27 -0
- data/lib/webhookdb/liquid/filters.rb +16 -0
- data/lib/webhookdb/liquid/liquification.rb +26 -0
- data/lib/webhookdb/liquid/partial.rb +12 -0
- data/lib/webhookdb/logged_webhook/resilient.rb +95 -0
- data/lib/webhookdb/logged_webhook.rb +194 -0
- data/lib/webhookdb/message/body.rb +25 -0
- data/lib/webhookdb/message/delivery.rb +127 -0
- data/lib/webhookdb/message/email_transport.rb +133 -0
- data/lib/webhookdb/message/fake_transport.rb +54 -0
- data/lib/webhookdb/message/liquid_drops.rb +29 -0
- data/lib/webhookdb/message/template.rb +89 -0
- data/lib/webhookdb/message/transport.rb +43 -0
- data/lib/webhookdb/message.rb +150 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +42 -0
- data/lib/webhookdb/messages/invite.rb +23 -0
- data/lib/webhookdb/messages/new_customer.rb +14 -0
- data/lib/webhookdb/messages/org_database_migration_finished.rb +23 -0
- data/lib/webhookdb/messages/org_database_migration_started.rb +24 -0
- data/lib/webhookdb/messages/specs.rb +57 -0
- data/lib/webhookdb/messages/verification.rb +23 -0
- data/lib/webhookdb/method_utilities.rb +82 -0
- data/lib/webhookdb/microsoft_calendar.rb +36 -0
- data/lib/webhookdb/nextpax.rb +14 -0
- data/lib/webhookdb/oauth/front.rb +58 -0
- data/lib/webhookdb/oauth/intercom.rb +58 -0
- data/lib/webhookdb/oauth/session.rb +24 -0
- data/lib/webhookdb/oauth.rb +80 -0
- data/lib/webhookdb/organization/alerting.rb +35 -0
- data/lib/webhookdb/organization/database_migration.rb +151 -0
- data/lib/webhookdb/organization/db_builder.rb +429 -0
- data/lib/webhookdb/organization.rb +506 -0
- data/lib/webhookdb/organization_membership.rb +58 -0
- data/lib/webhookdb/phone_number.rb +38 -0
- data/lib/webhookdb/plaid.rb +23 -0
- data/lib/webhookdb/platform.rb +27 -0
- data/lib/webhookdb/plivo.rb +52 -0
- data/lib/webhookdb/postgres/maintenance.rb +166 -0
- data/lib/webhookdb/postgres/model.rb +82 -0
- data/lib/webhookdb/postgres/model_utilities.rb +382 -0
- data/lib/webhookdb/postgres/testing_pixie.rb +16 -0
- data/lib/webhookdb/postgres/validations.rb +46 -0
- data/lib/webhookdb/postgres.rb +176 -0
- data/lib/webhookdb/postmark.rb +20 -0
- data/lib/webhookdb/redis.rb +35 -0
- data/lib/webhookdb/replicator/atom_single_feed_v1.rb +116 -0
- data/lib/webhookdb/replicator/aws_pricing_v1.rb +488 -0
- data/lib/webhookdb/replicator/base.rb +1185 -0
- data/lib/webhookdb/replicator/column.rb +482 -0
- data/lib/webhookdb/replicator/convertkit_broadcast_v1.rb +69 -0
- data/lib/webhookdb/replicator/convertkit_subscriber_v1.rb +200 -0
- data/lib/webhookdb/replicator/convertkit_tag_v1.rb +66 -0
- data/lib/webhookdb/replicator/convertkit_v1_mixin.rb +65 -0
- data/lib/webhookdb/replicator/docgen.rb +167 -0
- data/lib/webhookdb/replicator/email_octopus_campaign_v1.rb +84 -0
- data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +159 -0
- data/lib/webhookdb/replicator/email_octopus_event_v1.rb +244 -0
- data/lib/webhookdb/replicator/email_octopus_list_v1.rb +101 -0
- data/lib/webhookdb/replicator/fake.rb +453 -0
- data/lib/webhookdb/replicator/front_conversation_v1.rb +45 -0
- data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +55 -0
- data/lib/webhookdb/replicator/front_message_v1.rb +45 -0
- data/lib/webhookdb/replicator/front_v1_mixin.rb +22 -0
- data/lib/webhookdb/replicator/github_issue_comment_v1.rb +58 -0
- data/lib/webhookdb/replicator/github_issue_v1.rb +83 -0
- data/lib/webhookdb/replicator/github_pull_v1.rb +84 -0
- data/lib/webhookdb/replicator/github_release_v1.rb +47 -0
- data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +250 -0
- data/lib/webhookdb/replicator/github_repository_event_v1.rb +45 -0
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +465 -0
- data/lib/webhookdb/replicator/icalendar_event_v1.rb +334 -0
- data/lib/webhookdb/replicator/increase_account_number_v1.rb +77 -0
- data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +61 -0
- data/lib/webhookdb/replicator/increase_account_v1.rb +63 -0
- data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +64 -0
- data/lib/webhookdb/replicator/increase_limit_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_transaction_v1.rb +74 -0
- data/lib/webhookdb/replicator/increase_v1_mixin.rb +121 -0
- data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +61 -0
- data/lib/webhookdb/replicator/intercom_contact_v1.rb +36 -0
- data/lib/webhookdb/replicator/intercom_conversation_v1.rb +38 -0
- data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +69 -0
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +105 -0
- data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +65 -0
- data/lib/webhookdb/replicator/plivo_sms_inbound_v1.rb +102 -0
- data/lib/webhookdb/replicator/postmark_inbound_message_v1.rb +94 -0
- data/lib/webhookdb/replicator/postmark_outbound_message_event_v1.rb +107 -0
- data/lib/webhookdb/replicator/schema_modification.rb +42 -0
- data/lib/webhookdb/replicator/shopify_customer_v1.rb +58 -0
- data/lib/webhookdb/replicator/shopify_order_v1.rb +64 -0
- data/lib/webhookdb/replicator/shopify_v1_mixin.rb +161 -0
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +169 -0
- data/lib/webhookdb/replicator/sponsy_customer_v1.rb +54 -0
- data/lib/webhookdb/replicator/sponsy_placement_v1.rb +34 -0
- data/lib/webhookdb/replicator/sponsy_publication_v1.rb +125 -0
- data/lib/webhookdb/replicator/sponsy_slot_v1.rb +41 -0
- data/lib/webhookdb/replicator/sponsy_status_v1.rb +35 -0
- data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +165 -0
- data/lib/webhookdb/replicator/state_machine_step.rb +69 -0
- data/lib/webhookdb/replicator/stripe_charge_v1.rb +77 -0
- data/lib/webhookdb/replicator/stripe_coupon_v1.rb +62 -0
- data/lib/webhookdb/replicator/stripe_customer_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_dispute_v1.rb +77 -0
- data/lib/webhookdb/replicator/stripe_invoice_item_v1.rb +82 -0
- data/lib/webhookdb/replicator/stripe_invoice_v1.rb +116 -0
- data/lib/webhookdb/replicator/stripe_payout_v1.rb +67 -0
- data/lib/webhookdb/replicator/stripe_price_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_product_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_refund_v1.rb +101 -0
- data/lib/webhookdb/replicator/stripe_subscription_item_v1.rb +56 -0
- data/lib/webhookdb/replicator/stripe_subscription_v1.rb +75 -0
- data/lib/webhookdb/replicator/stripe_v1_mixin.rb +116 -0
- data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +141 -0
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +169 -0
- data/lib/webhookdb/replicator/transistor_show_v1.rb +68 -0
- data/lib/webhookdb/replicator/transistor_v1_mixin.rb +65 -0
- data/lib/webhookdb/replicator/twilio_sms_v1.rb +156 -0
- data/lib/webhookdb/replicator/webhook_request.rb +5 -0
- data/lib/webhookdb/replicator/webhookdb_customer_v1.rb +74 -0
- data/lib/webhookdb/replicator.rb +224 -0
- data/lib/webhookdb/role.rb +42 -0
- data/lib/webhookdb/sentry.rb +35 -0
- data/lib/webhookdb/service/auth.rb +138 -0
- data/lib/webhookdb/service/collection.rb +91 -0
- data/lib/webhookdb/service/entities.rb +97 -0
- data/lib/webhookdb/service/helpers.rb +270 -0
- data/lib/webhookdb/service/middleware.rb +124 -0
- data/lib/webhookdb/service/types.rb +30 -0
- data/lib/webhookdb/service/validators.rb +32 -0
- data/lib/webhookdb/service/view_api.rb +63 -0
- data/lib/webhookdb/service.rb +219 -0
- data/lib/webhookdb/service_integration.rb +332 -0
- data/lib/webhookdb/shopify.rb +35 -0
- data/lib/webhookdb/signalwire.rb +13 -0
- data/lib/webhookdb/slack.rb +68 -0
- data/lib/webhookdb/snowflake.rb +90 -0
- data/lib/webhookdb/spec_helpers/async.rb +122 -0
- data/lib/webhookdb/spec_helpers/citest.rb +88 -0
- data/lib/webhookdb/spec_helpers/integration.rb +121 -0
- data/lib/webhookdb/spec_helpers/message.rb +41 -0
- data/lib/webhookdb/spec_helpers/postgres.rb +220 -0
- data/lib/webhookdb/spec_helpers/service.rb +432 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +56 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +915 -0
- data/lib/webhookdb/spec_helpers/whdb.rb +139 -0
- data/lib/webhookdb/spec_helpers.rb +63 -0
- data/lib/webhookdb/sponsy.rb +14 -0
- data/lib/webhookdb/stripe.rb +37 -0
- data/lib/webhookdb/subscription.rb +203 -0
- data/lib/webhookdb/sync_target.rb +491 -0
- data/lib/webhookdb/tasks/admin.rb +49 -0
- data/lib/webhookdb/tasks/annotate.rb +36 -0
- data/lib/webhookdb/tasks/db.rb +82 -0
- data/lib/webhookdb/tasks/docs.rb +42 -0
- data/lib/webhookdb/tasks/fixture.rb +35 -0
- data/lib/webhookdb/tasks/message.rb +50 -0
- data/lib/webhookdb/tasks/regress.rb +87 -0
- data/lib/webhookdb/tasks/release.rb +27 -0
- data/lib/webhookdb/tasks/sidekiq.rb +23 -0
- data/lib/webhookdb/tasks/specs.rb +64 -0
- data/lib/webhookdb/theranest.rb +15 -0
- data/lib/webhookdb/transistor.rb +13 -0
- data/lib/webhookdb/twilio.rb +13 -0
- data/lib/webhookdb/typed_struct.rb +44 -0
- data/lib/webhookdb/version.rb +5 -0
- data/lib/webhookdb/webhook_response.rb +50 -0
- data/lib/webhookdb/webhook_subscription/delivery.rb +82 -0
- data/lib/webhookdb/webhook_subscription.rb +226 -0
- data/lib/webhookdb/windows_tz.rb +32 -0
- data/lib/webhookdb/xml.rb +92 -0
- data/lib/webhookdb.rb +224 -0
- data/lib/webterm/apps.rb +45 -0
- 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
|