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.
- 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,332 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webhookdb/formatting"
|
|
4
|
+
require "webhookdb/id"
|
|
5
|
+
require "webhookdb/postgres/model"
|
|
6
|
+
require "sequel/plugins/soft_deletes"
|
|
7
|
+
|
|
8
|
+
class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integrations)
|
|
9
|
+
class TableRenameError < Webhookdb::InvalidInput; end
|
|
10
|
+
|
|
11
|
+
# We limit the information that a user can access through the CLI to these fields.
|
|
12
|
+
# Blank string returns all info.
|
|
13
|
+
INTEGRATION_INFO_FIELDS = ["id", "service", "table", "url", "webhook_secret", ""].freeze
|
|
14
|
+
|
|
15
|
+
plugin :timestamps
|
|
16
|
+
plugin :column_encryption do |enc|
|
|
17
|
+
enc.column :data_encryption_secret
|
|
18
|
+
enc.column :webhook_secret
|
|
19
|
+
enc.column :backfill_key
|
|
20
|
+
enc.column :backfill_secret
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
many_to_one :organization, class: "Webhookdb::Organization"
|
|
24
|
+
one_to_many :webhook_subscriptions, class: "Webhookdb::WebhookSubscription"
|
|
25
|
+
one_to_many :all_webhook_subscriptions,
|
|
26
|
+
class: "Webhookdb::WebhookSubscription",
|
|
27
|
+
readonly: true,
|
|
28
|
+
dataset: (
|
|
29
|
+
lambda do |r|
|
|
30
|
+
r.associated_dataset.where(
|
|
31
|
+
Sequel[organization_id:] | Sequel[service_integration_id: id],
|
|
32
|
+
)
|
|
33
|
+
end),
|
|
34
|
+
eager_loader: (
|
|
35
|
+
lambda do |eo|
|
|
36
|
+
sint_ids = eo[:id_map].keys
|
|
37
|
+
org_ids_for_sints = eo[:rows].to_h { |r| [r.id, r.organization_id] }
|
|
38
|
+
all_subs = Webhookdb::WebhookSubscription.
|
|
39
|
+
left_join(:service_integrations, {id: :service_integration_id}).
|
|
40
|
+
select(Sequel[:webhook_subscriptions][Sequel.lit("*")]).
|
|
41
|
+
where(
|
|
42
|
+
Sequel[Sequel[:webhook_subscriptions][:organization_id] => org_ids_for_sints.values.uniq] |
|
|
43
|
+
Sequel[Sequel[:webhook_subscriptions][:service_integration_id] => sint_ids],
|
|
44
|
+
).all
|
|
45
|
+
subs_by_sint = {}
|
|
46
|
+
subs_by_org = {}
|
|
47
|
+
all_subs.each do |sub|
|
|
48
|
+
if (orgid = sub[:organization_id])
|
|
49
|
+
subs = subs_by_org[orgid] ||= []
|
|
50
|
+
else
|
|
51
|
+
sint_id = sub[:service_integration_id]
|
|
52
|
+
subs = subs_by_sint[sint_id] ||= []
|
|
53
|
+
end
|
|
54
|
+
subs << sub
|
|
55
|
+
end
|
|
56
|
+
eo[:rows].each do |sint|
|
|
57
|
+
subs = subs_by_sint.fetch(sint.id, [])
|
|
58
|
+
subs.concat(subs_by_org.fetch(sint.organization_id, []))
|
|
59
|
+
sint.associations[:all_webhook_subscriptions] = subs
|
|
60
|
+
end
|
|
61
|
+
end)
|
|
62
|
+
|
|
63
|
+
many_to_one :depends_on, class: self
|
|
64
|
+
one_to_many :dependents, key: :depends_on_id, class: self
|
|
65
|
+
one_to_many :sync_targets, class: "Webhookdb::SyncTarget"
|
|
66
|
+
|
|
67
|
+
def self.create_disambiguated(service_name, **kwargs)
|
|
68
|
+
kwargs[:table_name] ||= "#{service_name}_#{SecureRandom.hex(2)}"
|
|
69
|
+
return self.create(service_name:, **kwargs)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def can_be_modified_by?(customer)
|
|
73
|
+
return customer.verified_member_of?(self.organization)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Webhookdb::Replicator::Base]
|
|
77
|
+
def replicator
|
|
78
|
+
return Webhookdb::Replicator.create(self)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def log_tags
|
|
82
|
+
return {
|
|
83
|
+
service_integration_id: self.id,
|
|
84
|
+
service_integration_name: self.service_name,
|
|
85
|
+
service_integration_table: self.table_name,
|
|
86
|
+
**self.organization.log_tags,
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def authed_api_path
|
|
91
|
+
return "/v1/organizations/#{self.organization_id}/service_integrations/#{self.opaque_id}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def unauthed_webhook_path
|
|
95
|
+
return "/v1/service_integrations/#{self.opaque_id}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def unauthed_webhook_endpoint
|
|
99
|
+
return Webhookdb.api_url + self.unauthed_webhook_path
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def plan_supports_integration?
|
|
103
|
+
# if the sint's organization has an active subscription, return true
|
|
104
|
+
return true if self.organization.active_subscription?
|
|
105
|
+
# if there is no active subscription, check whether the integration is one of the first two
|
|
106
|
+
# created by the organization
|
|
107
|
+
limit = Webhookdb::Subscription.max_free_integrations
|
|
108
|
+
free_integrations = Webhookdb::ServiceIntegration.
|
|
109
|
+
where(organization: self.organization).order(:created_at, :id).limit(limit).all
|
|
110
|
+
free_integrations.each do |sint|
|
|
111
|
+
return true if sint.id == self.id
|
|
112
|
+
end
|
|
113
|
+
# if not, the integration is not supported
|
|
114
|
+
return false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Return service integrations that can be used as the dependency
|
|
118
|
+
# for this integration.
|
|
119
|
+
# @return [Array<Webhookdb::ServiceIntegration>]
|
|
120
|
+
def dependency_candidates
|
|
121
|
+
dep_descr = self.replicator.descriptor.dependency_descriptor
|
|
122
|
+
return [] if dep_descr.nil?
|
|
123
|
+
return self.organization.service_integrations.
|
|
124
|
+
select { |si| si.service_name == dep_descr.name }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def recursive_dependents
|
|
128
|
+
return self.dependents + self.dependents.flat_map(&:recursive_dependents)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def destroy_self_and_all_dependents
|
|
132
|
+
self.dependents.each(&:destroy_self_and_all_dependents)
|
|
133
|
+
|
|
134
|
+
begin
|
|
135
|
+
self.replicator.admin_dataset(timeout: :fast) { |ds| ds.db << "DROP TABLE #{self.table_name}" }
|
|
136
|
+
rescue Sequel::DatabaseError => e
|
|
137
|
+
raise unless e.wrapped_exception.is_a?(PG::UndefinedTable)
|
|
138
|
+
end
|
|
139
|
+
self.destroy
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class Stats
|
|
143
|
+
attr_reader :message, :data
|
|
144
|
+
|
|
145
|
+
def initialize(message, data)
|
|
146
|
+
@message = message
|
|
147
|
+
@data = data
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def display_headers
|
|
151
|
+
return [
|
|
152
|
+
[:count_last_7_days_formatted, "Count Last 7 Days"],
|
|
153
|
+
[:success_last_7_days_formatted, "Successful Last 7 Days"],
|
|
154
|
+
[:success_last_7_days_percent_formatted, "Successful Last 7 Days %"],
|
|
155
|
+
[:rejected_last_7_days_formatted, "Rejected Last 7 Days"],
|
|
156
|
+
[:rejected_last_7_days_percent_formatted, "Rejected Last 7 Days %"],
|
|
157
|
+
[:successful_of_last_10_formatted, "Successful Of Last 10 Webhooks"],
|
|
158
|
+
[:rejected_of_last_10_formatted, "Rejected Of Last 10 Webhooks"],
|
|
159
|
+
]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def as_json(*_o)
|
|
163
|
+
return @data.merge(message: @message, display_headers: self.display_headers)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [Webhookdb::ServiceIntegration::Stats]
|
|
168
|
+
def stats
|
|
169
|
+
all_logged_webhooks = Webhookdb::LoggedWebhook.where(
|
|
170
|
+
service_integration_opaque_id: self.opaque_id,
|
|
171
|
+
).where { inserted_at > 7.days.ago }
|
|
172
|
+
|
|
173
|
+
if all_logged_webhooks.empty?
|
|
174
|
+
return Stats.new(
|
|
175
|
+
"We have no record of receiving webhooks for that integration in the past seven days.",
|
|
176
|
+
{},
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# rubocop:disable Naming/VariableNumber
|
|
181
|
+
count_last_7_days = all_logged_webhooks.count
|
|
182
|
+
rejected_last_7_days = all_logged_webhooks.where { response_status >= 400 }.count
|
|
183
|
+
success_last_7_days = (count_last_7_days - rejected_last_7_days)
|
|
184
|
+
rejected_last_7_days_percent = (rejected_last_7_days.to_f / count_last_7_days)
|
|
185
|
+
success_last_7_days_percent = (success_last_7_days.to_f / count_last_7_days)
|
|
186
|
+
last_10 = Webhookdb::LoggedWebhook.order_by(Sequel.desc(:inserted_at)).limit(10).select_map(:response_status)
|
|
187
|
+
last_10_success, last_10_rejected = last_10.partition { |rs| rs < 400 }
|
|
188
|
+
|
|
189
|
+
data = {
|
|
190
|
+
count_last_7_days:,
|
|
191
|
+
count_last_7_days_formatted: count_last_7_days.to_s,
|
|
192
|
+
success_last_7_days:,
|
|
193
|
+
success_last_7_days_formatted: success_last_7_days.to_s,
|
|
194
|
+
success_last_7_days_percent:,
|
|
195
|
+
success_last_7_days_percent_formatted: "%.1f%%" % (success_last_7_days_percent * 100),
|
|
196
|
+
rejected_last_7_days:,
|
|
197
|
+
rejected_last_7_days_formatted: rejected_last_7_days.to_s,
|
|
198
|
+
rejected_last_7_days_percent:,
|
|
199
|
+
rejected_last_7_days_percent_formatted: "%.1f%%" % (rejected_last_7_days_percent * 100),
|
|
200
|
+
successful_of_last_10: last_10_success.size,
|
|
201
|
+
successful_of_last_10_formatted: last_10_success.size.to_s,
|
|
202
|
+
rejected_of_last_10: last_10_rejected.size,
|
|
203
|
+
rejected_of_last_10_formatted: last_10_rejected.size.to_s,
|
|
204
|
+
}
|
|
205
|
+
# rubocop:enable Naming/VariableNumber
|
|
206
|
+
return Stats.new("", data)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def rename_table(to:)
|
|
210
|
+
Webhookdb::Organization::DatabaseMigration.guard_ongoing!(self.organization)
|
|
211
|
+
unless Webhookdb::DBAdapter::VALID_IDENTIFIER.match?(to)
|
|
212
|
+
msg = "Sorry, this is not a valid table name. " + Webhookdb::DBAdapter::INVALID_IDENTIFIER_MESSAGE
|
|
213
|
+
msg += " And we see you what you did there ;)" if to.include?(";") && to.downcase.include?("drop")
|
|
214
|
+
raise TableRenameError, msg
|
|
215
|
+
end
|
|
216
|
+
self.db.transaction do
|
|
217
|
+
begin
|
|
218
|
+
self.organization.admin_connection { |db| db << "ALTER TABLE #{self.table_name} RENAME TO #{to}" }
|
|
219
|
+
rescue Sequel::DatabaseError => e
|
|
220
|
+
case e.wrapped_exception
|
|
221
|
+
when PG::DuplicateTable
|
|
222
|
+
raise TableRenameError,
|
|
223
|
+
"There is already a table named \"#{to}\". Run `webhookdb db tables` to see available tables."
|
|
224
|
+
when PG::SyntaxError
|
|
225
|
+
raise TableRenameError,
|
|
226
|
+
"Please try again with double quotes around '#{to}' since it contains invalid identifier characters."
|
|
227
|
+
else
|
|
228
|
+
raise e
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
self.update(table_name: to)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def requires_sequence?
|
|
236
|
+
return self.replicator.requires_sequence?
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def sequence_name
|
|
240
|
+
return "replicator_seq_org_#{self.organization_id}_#{self.service_name}_#{self.id}_seq"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def ensure_sequence(skip_check: false)
|
|
244
|
+
self.db << self.ensure_sequence_sql(skip_check:)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def ensure_sequence_sql(skip_check: false)
|
|
248
|
+
raise Webhookdb::InvalidPrecondition, "#{self.service_name} does not require sequence" if
|
|
249
|
+
!skip_check && !self.requires_sequence?
|
|
250
|
+
return "CREATE SEQUENCE IF NOT EXISTS #{self.sequence_name}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def sequence_nextval
|
|
254
|
+
return self.db.select(Sequel.function(:nextval, self.sequence_name)).single_value
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
#
|
|
258
|
+
# :Sequel Hooks:
|
|
259
|
+
#
|
|
260
|
+
|
|
261
|
+
def before_create
|
|
262
|
+
self[:opaque_id] ||= Webhookdb::Id.new_opaque_id("svi")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @!attribute organization
|
|
266
|
+
# @return [Webhookdb::Organization]
|
|
267
|
+
|
|
268
|
+
# @!attribute table_name
|
|
269
|
+
# @return [String] Name of the table
|
|
270
|
+
|
|
271
|
+
# @!attribute service_name
|
|
272
|
+
# @return [String] Lookup name of the service
|
|
273
|
+
|
|
274
|
+
# @!attribute opaque_id
|
|
275
|
+
# @return [String]
|
|
276
|
+
|
|
277
|
+
# @!attribute api_url
|
|
278
|
+
# @return [String] Root Url of the api to backfill from
|
|
279
|
+
|
|
280
|
+
# @!attribute backfill_key
|
|
281
|
+
# @return [String] Key for backfilling.
|
|
282
|
+
|
|
283
|
+
# @!attribute backfill_secret
|
|
284
|
+
# @return [String] Password/secret for backfilling.
|
|
285
|
+
|
|
286
|
+
# @!attribute webhook_secret
|
|
287
|
+
# @return [String] Secret used to sign webhooks.
|
|
288
|
+
|
|
289
|
+
# @!attribute depends_on
|
|
290
|
+
# @return [Webhookdb::ServiceIntegration]
|
|
291
|
+
|
|
292
|
+
# @!attribute data_encryption_secret
|
|
293
|
+
# @return [String] The encryption key used to encrypt data for this organization.
|
|
294
|
+
# Note that this field is itself encrypted using Sequel encryption;
|
|
295
|
+
# its decrypted value is meant to be used as the data encryption key.
|
|
296
|
+
|
|
297
|
+
# @!attribute skip_webhook_verification
|
|
298
|
+
# @return [Boolean] Set this to disable webhook verification on this integration.
|
|
299
|
+
# Useful when replaying logged webhooks.
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Table: service_integrations
|
|
303
|
+
# -----------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
304
|
+
# Columns:
|
|
305
|
+
# id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
|
|
306
|
+
# created_at | timestamp with time zone | NOT NULL DEFAULT now()
|
|
307
|
+
# updated_at | timestamp with time zone |
|
|
308
|
+
# organization_id | integer | NOT NULL
|
|
309
|
+
# api_url | text | NOT NULL DEFAULT ''::text
|
|
310
|
+
# opaque_id | text | NOT NULL
|
|
311
|
+
# service_name | text | NOT NULL
|
|
312
|
+
# webhook_secret | text |
|
|
313
|
+
# table_name | text | NOT NULL
|
|
314
|
+
# backfill_key | text |
|
|
315
|
+
# backfill_secret | text |
|
|
316
|
+
# last_backfilled_at | timestamp with time zone |
|
|
317
|
+
# depends_on_id | integer |
|
|
318
|
+
# data_encryption_secret | text |
|
|
319
|
+
# skip_webhook_verification | boolean | NOT NULL DEFAULT false
|
|
320
|
+
# Indexes:
|
|
321
|
+
# service_integrations_pkey | PRIMARY KEY btree (id)
|
|
322
|
+
# service_integrations_opaque_id_key | UNIQUE btree (opaque_id)
|
|
323
|
+
# unique_tablename_in_org | UNIQUE btree (organization_id, table_name)
|
|
324
|
+
# Foreign key constraints:
|
|
325
|
+
# service_integrations_depends_on_id_fkey | (depends_on_id) REFERENCES service_integrations(id) ON DELETE RESTRICT
|
|
326
|
+
# service_integrations_organization_id_fkey | (organization_id) REFERENCES organizations(id)
|
|
327
|
+
# Referenced By:
|
|
328
|
+
# backfill_jobs | backfill_jobs_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id) ON DELETE CASCADE
|
|
329
|
+
# service_integrations | service_integrations_depends_on_id_fkey | (depends_on_id) REFERENCES service_integrations(id) ON DELETE RESTRICT
|
|
330
|
+
# sync_targets | sync_targets_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id) ON DELETE CASCADE
|
|
331
|
+
# webhook_subscriptions | webhook_subscriptions_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id)
|
|
332
|
+
# -----------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/security_utils"
|
|
4
|
+
|
|
5
|
+
class Webhookdb::Shopify
|
|
6
|
+
include Appydays::Configurable
|
|
7
|
+
|
|
8
|
+
configurable(:shopify) do
|
|
9
|
+
setting :http_timeout, 30
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# This function is used in the backfill process to parse out the
|
|
13
|
+
# pagination_token from the responses
|
|
14
|
+
def self.parse_link_header(header)
|
|
15
|
+
parts = header.split(",")
|
|
16
|
+
|
|
17
|
+
parts.to_h do |part, _|
|
|
18
|
+
section = part.split(";")
|
|
19
|
+
name = section[1][/rel="(.*)"/, 1].to_sym
|
|
20
|
+
url = section[0][/<(.*)>/, 1]
|
|
21
|
+
# results = section[2][/results="(.*)"/, 1] == 'true'
|
|
22
|
+
|
|
23
|
+
[name, url]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Compare the computed HMAC digest based on the shared secret and the
|
|
28
|
+
# request contents to the reported HMAC in the headers
|
|
29
|
+
#
|
|
30
|
+
# see https://shopify.dev/tutorials/manage-webhooks#verifying-webhooks
|
|
31
|
+
def self.verify_webhook(data, hmac_header, webhook_secret)
|
|
32
|
+
calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", webhook_secret, data))
|
|
33
|
+
return ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "appydays/configurable"
|
|
4
|
+
require "appydays/loggable"
|
|
5
|
+
|
|
6
|
+
module Webhookdb::Signalwire
|
|
7
|
+
include Appydays::Configurable
|
|
8
|
+
include Appydays::Loggable
|
|
9
|
+
|
|
10
|
+
configurable(:signalwire) do
|
|
11
|
+
setting :http_timeout, 30
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "appydays/configurable"
|
|
4
|
+
require "slack-notifier"
|
|
5
|
+
|
|
6
|
+
require "webhookdb"
|
|
7
|
+
|
|
8
|
+
class Webhookdb::Slack
|
|
9
|
+
include Appydays::Configurable
|
|
10
|
+
extend Webhookdb::MethodUtilities
|
|
11
|
+
|
|
12
|
+
# Set this during testing
|
|
13
|
+
singleton_attr_accessor :http_client
|
|
14
|
+
@http_client = nil
|
|
15
|
+
|
|
16
|
+
configurable(:slack) do
|
|
17
|
+
setting :webhook_url, "http://unconfigured-slack-webhook"
|
|
18
|
+
setting :channel_override, nil
|
|
19
|
+
setting :suppress_all, false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.new_notifier(opts={})
|
|
23
|
+
opts[:channel] ||= "#eng-naboo"
|
|
24
|
+
opts[:username] ||= "Unknown"
|
|
25
|
+
opts[:icon_emoji] ||= ":question:"
|
|
26
|
+
opts[:channel] = self.channel_override if self.channel_override
|
|
27
|
+
if (force_chan = opts.delete(:force_channel))
|
|
28
|
+
opts[:channel] = force_chan
|
|
29
|
+
end
|
|
30
|
+
return ::Slack::Notifier.new(self.webhook_url) do
|
|
31
|
+
defaults opts
|
|
32
|
+
if Webhookdb::Slack.suppress_all
|
|
33
|
+
http_client NoOpHttpClient.new
|
|
34
|
+
elsif Webhookdb::Slack.http_client
|
|
35
|
+
http_client Webhookdb::Slack.http_client
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.ignore_channel_not_found
|
|
41
|
+
yield()
|
|
42
|
+
rescue ::Slack::Notifier::APIError => e
|
|
43
|
+
return if e.message.include?("channel_not_found")
|
|
44
|
+
return if e.message.include?("channel_is_archived")
|
|
45
|
+
raise e
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.post_many(channels, notifier_options: {}, payload: {})
|
|
49
|
+
channels.each do |chan|
|
|
50
|
+
notifier = self.new_notifier(notifier_options.merge(channel: chan))
|
|
51
|
+
self.ignore_channel_not_found do
|
|
52
|
+
notifier.post(payload)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class NoOpHttpClient
|
|
58
|
+
attr_reader :posts
|
|
59
|
+
|
|
60
|
+
def initialize
|
|
61
|
+
@posts = []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def post(uri, params={})
|
|
65
|
+
self.posts << [uri, params]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "oj"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
class Webhookdb::Snowflake
|
|
7
|
+
include Appydays::Configurable
|
|
8
|
+
include Appydays::Loggable
|
|
9
|
+
|
|
10
|
+
configurable(:snowflake) do
|
|
11
|
+
setting :run_tests, false
|
|
12
|
+
setting :test_url, "snowflake://user:pwd@host/dbname"
|
|
13
|
+
setting :snowsql, "snowsql"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Given a Snowflake URL, return the command line args.
|
|
17
|
+
# Args for the commandline can be traditional URL pieces (host -> account, user/password, etc),
|
|
18
|
+
# or passed as query params.
|
|
19
|
+
# Rules are:
|
|
20
|
+
# - Any query param exactly matching accountname/username/dbname/schemaname/rolename/warehouse
|
|
21
|
+
# is used.
|
|
22
|
+
# - Any query param matching account/user/db/schema/role is used.
|
|
23
|
+
# - URI hostname is used as accountname, basic auth user as username, and uri path as dbname.
|
|
24
|
+
# - Password is pulled from query param 'password' or uri basic auth password.
|
|
25
|
+
def self.parse_url_to_cli_args(url, format: "json")
|
|
26
|
+
uri = URI(url)
|
|
27
|
+
params = Rack::Utils.parse_query(uri.query)
|
|
28
|
+
password = params["password"] || uri.password
|
|
29
|
+
raise ArgumentError, "must provide password in uri basic auth or query params" if password.blank?
|
|
30
|
+
cli = [
|
|
31
|
+
self.snowsql,
|
|
32
|
+
"-o", "friendly=false",
|
|
33
|
+
"-o", "output_format=#{format}",
|
|
34
|
+
"-o", "timing=false",
|
|
35
|
+
"--accountname", params["accountname"] || params["account"] || uri.hostname || "",
|
|
36
|
+
"--username", params["username"] || params["user"] || uri.user || "",
|
|
37
|
+
"--dbname", params["dbname"] || params["db"] || uri.path&.delete_prefix("/") || "",
|
|
38
|
+
]
|
|
39
|
+
raise ArgumentError, "url requires account (host), user, and db (or uri path): #{url}" if cli.include?("")
|
|
40
|
+
|
|
41
|
+
if (schemaname = params["schemaname"] || params["schema"]).present?
|
|
42
|
+
cli.push("--schemaname", schemaname)
|
|
43
|
+
end
|
|
44
|
+
if (rolename = params["rolename"] || params["role"]).present?
|
|
45
|
+
cli.push("--rolename", rolename)
|
|
46
|
+
end
|
|
47
|
+
cli.push("--warehouse", params["warehouse"]) if params["warehouse"].present?
|
|
48
|
+
return cli, {"SNOWSQL_PWD" => password}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.run_cli(url, query, parse: false, format: "json")
|
|
52
|
+
args, env = self.parse_url_to_cli_args(url, format:)
|
|
53
|
+
args.push("-q", query)
|
|
54
|
+
stdout, stderr, status = Open3.capture3(env, *args)
|
|
55
|
+
|
|
56
|
+
if stderr.blank? && status.success?
|
|
57
|
+
result = if parse.respond_to?(:call)
|
|
58
|
+
parse.call(stdout)
|
|
59
|
+
elsif parse && format == "json"
|
|
60
|
+
self._parse_json(stdout)
|
|
61
|
+
else
|
|
62
|
+
stdout
|
|
63
|
+
end
|
|
64
|
+
return result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
self.logger.error("snowflake_error", stdout:, stderr:, status:, cli_args: args)
|
|
68
|
+
msg = "status: #{status}, stderr: #{stderr}, stdout: #{stdout}, query: #{query}"
|
|
69
|
+
raise Webhookdb::InvalidPostcondition, "snowflake failed: #{msg}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# We can't parse newline delimited json easily, so split it ourselves and parse each document.
|
|
73
|
+
def self._parse_json(stdout)
|
|
74
|
+
docs = stdout.split("]\n[")
|
|
75
|
+
result = docs.each_with_index.map do |j, i|
|
|
76
|
+
if docs.size > 1
|
|
77
|
+
if i.zero?
|
|
78
|
+
j += "]"
|
|
79
|
+
else
|
|
80
|
+
j = "[" + j
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
Oj.load(j.strip)
|
|
84
|
+
end
|
|
85
|
+
return result
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
msg = "error: #{e}, stdout: #{stdout}"
|
|
88
|
+
raise Webhookdb::InvalidPostcondition, "Error parsing snowsql output: #{msg}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sidekiq/testing"
|
|
4
|
+
|
|
5
|
+
require "webhookdb/async"
|
|
6
|
+
require "webhookdb/slack"
|
|
7
|
+
require "webhookdb/spec_helpers"
|
|
8
|
+
|
|
9
|
+
module Webhookdb::SpecHelpers::Async
|
|
10
|
+
def self.included(context)
|
|
11
|
+
Sidekiq::Testing.inline!
|
|
12
|
+
Amigo::QueueBackoffJob.reset
|
|
13
|
+
|
|
14
|
+
context.before(:each) do |example|
|
|
15
|
+
if (sidekiq_mode = example.metadata[:sidekiq])
|
|
16
|
+
Sidekiq::Testing.send(:"#{sidekiq_mode}!")
|
|
17
|
+
else
|
|
18
|
+
Sidekiq::Testing.inline!
|
|
19
|
+
end
|
|
20
|
+
Webhookdb::Postgres.do_not_defer_events = true if example.metadata[:do_not_defer_events]
|
|
21
|
+
if example.metadata[:slack]
|
|
22
|
+
Webhookdb::Slack.http_client = Webhookdb::Slack::NoOpHttpClient.new
|
|
23
|
+
Webhookdb::Slack.suppress_all = false
|
|
24
|
+
end
|
|
25
|
+
if example.metadata[:sentry]
|
|
26
|
+
Webhookdb::Sentry.dsn = "http://public:secret@not-really-sentry.nope/someproject"
|
|
27
|
+
Webhookdb::Sentry.run_after_configured_hooks
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context.after(:each) do |example|
|
|
32
|
+
Webhookdb::Postgres.do_not_defer_events = false if example.metadata[:do_not_defer_events]
|
|
33
|
+
if example.metadata[:slack]
|
|
34
|
+
Webhookdb::Slack.http_client = nil
|
|
35
|
+
Webhookdb::Slack.reset_configuration
|
|
36
|
+
end
|
|
37
|
+
Webhookdb::Sentry.reset_configuration if example.metadata[:sentry]
|
|
38
|
+
Sidekiq::Queues.clear_all if example.metadata[:sidekiq] && Sidekiq::Testing.fake?
|
|
39
|
+
end
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module_function def job_hash(cls, **more)
|
|
44
|
+
params = {"class" => cls.to_s}
|
|
45
|
+
params.merge!(more.stringify_keys)
|
|
46
|
+
return hash_including(params)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
RSpec::Matchers.define(:have_queue) do |passed_name|
|
|
50
|
+
match do |sk|
|
|
51
|
+
raise "Sidekiq::Testing must be in fake mode" unless Sidekiq::Testing.fake?
|
|
52
|
+
raise ArgumentError, "argument must be Sidekiq, got #{sk.inspect}" unless sk == Sidekiq
|
|
53
|
+
@name = passed_name || "default"
|
|
54
|
+
q = Sidekiq::Queues[@name]
|
|
55
|
+
if @size
|
|
56
|
+
break true if @size.zero? && q.empty?
|
|
57
|
+
if q.size != @size
|
|
58
|
+
@_err = "has size #{q.size}, expected #{@size}"
|
|
59
|
+
break false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
if q.empty?
|
|
63
|
+
@_err = "is empty"
|
|
64
|
+
break false
|
|
65
|
+
end
|
|
66
|
+
(@matchers || []).each do |m|
|
|
67
|
+
expect(q).to include(m)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
failure_message do |*|
|
|
72
|
+
msg = "failed to match Sidekiq queue %s:" % @name
|
|
73
|
+
msg += " " + @_err if @_err
|
|
74
|
+
lines = [msg]
|
|
75
|
+
Sidekiq::Queues.jobs_by_queue.each do |n, jobs|
|
|
76
|
+
lines << " #{n}"
|
|
77
|
+
jobs.each do |j|
|
|
78
|
+
lines << " #{j}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
lines.join("\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
chain :named do |n|
|
|
85
|
+
@name = n
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
chain :including do |*matchers|
|
|
89
|
+
@matchers ||= []
|
|
90
|
+
@matchers.concat(*matchers)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
chain :consisting_of do |*matchers|
|
|
94
|
+
@matchers = matchers
|
|
95
|
+
@size = matchers.size
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
chain :of_size do |n|
|
|
99
|
+
@size = n
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
RSpec::Matchers.define(:have_empty_queues) do |*|
|
|
104
|
+
match do |sk|
|
|
105
|
+
raise "Sidekiq::Testing must be in fake mode" unless Sidekiq::Testing.fake?
|
|
106
|
+
raise ArgumentError, "argument must be Sidekiq, got #{sk.inspect}" unless sk == Sidekiq
|
|
107
|
+
@nonempty = Sidekiq::Queues.jobs_by_queue.select { |_n, jobs| jobs.size.positive? }
|
|
108
|
+
@nonempty.empty?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
failure_message do |*|
|
|
112
|
+
lines = ["Sidekiq queues have jobs:"]
|
|
113
|
+
Sidekiq::Queues.jobs_by_queue.each do |n, jobs|
|
|
114
|
+
lines << " #{n}"
|
|
115
|
+
jobs.each do |j|
|
|
116
|
+
lines << " #{j}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
lines.join("\n")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|