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,482 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webhookdb/db_adapter"
|
|
4
|
+
|
|
5
|
+
class Webhookdb::Replicator::Column
|
|
6
|
+
include Webhookdb::DBAdapter::ColumnTypes
|
|
7
|
+
|
|
8
|
+
class IsomorphicProc < Webhookdb::TypedStruct
|
|
9
|
+
attr_reader :ruby, :sql
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
NOT_IMPLEMENTED = ->(*) { raise NotImplementedError }
|
|
13
|
+
|
|
14
|
+
# Convert a Unix timestamp (fractional seconds) to a Datetime.
|
|
15
|
+
CONV_UNIX_TS = IsomorphicProc.new(
|
|
16
|
+
ruby: lambda do |i, **_|
|
|
17
|
+
return Time.at(i)
|
|
18
|
+
rescue TypeError
|
|
19
|
+
return nil
|
|
20
|
+
end,
|
|
21
|
+
sql: lambda do |i|
|
|
22
|
+
# We do not have the 'rescue TypeError' behavior here yet.
|
|
23
|
+
# It is a beast to add in because we can't easily check if something is convertable,
|
|
24
|
+
# nor can we easily exception handle without creating a stored function.
|
|
25
|
+
Sequel.function(:to_timestamp, Sequel.cast(i, :double))
|
|
26
|
+
end,
|
|
27
|
+
)
|
|
28
|
+
# Parse a value as an integer. Remove surrounding quotes.
|
|
29
|
+
CONV_TO_I = IsomorphicProc.new(
|
|
30
|
+
ruby: ->(i, **_) { i.nil? ? nil : i.delete_prefix('"').delete_suffix('"').to_i },
|
|
31
|
+
sql: ->(i) { Sequel.cast(i, :integer) },
|
|
32
|
+
)
|
|
33
|
+
# Given a Datetime, convert it to UTC and truncate to a Date.
|
|
34
|
+
CONV_TO_UTC_DATE = IsomorphicProc.new(
|
|
35
|
+
ruby: ->(t, **_) { t&.in_time_zone("UTC")&.to_date },
|
|
36
|
+
sql: lambda do |i|
|
|
37
|
+
ts = Sequel.cast(i, :timestamptz)
|
|
38
|
+
in_utc = Sequel.function(:timezone, "UTC", ts)
|
|
39
|
+
Sequel.cast(in_utc, :date)
|
|
40
|
+
end,
|
|
41
|
+
)
|
|
42
|
+
# Parse a value using Time.parse.
|
|
43
|
+
CONV_PARSE_TIME = IsomorphicProc.new(
|
|
44
|
+
ruby: ->(value, **_) { value.nil? ? nil : Time.parse(value) },
|
|
45
|
+
sql: ->(i) { Sequel.cast(i, :timestamptz) },
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Parse a value using Date.parse.
|
|
49
|
+
CONV_PARSE_DATE = IsomorphicProc.new(
|
|
50
|
+
ruby: ->(value, **_) { value.nil? ? nil : Date.parse(value) },
|
|
51
|
+
sql: ->(i) { Sequel.cast(i, :date) },
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
CONV_COMMA_SEP = IsomorphicProc.new(
|
|
55
|
+
ruby: ->(value, **_) { value.nil? ? [] : value.split(",").map(&:strip) },
|
|
56
|
+
sql: lambda do |_e, json_path:, source_col:|
|
|
57
|
+
e = source_col.get_text(json_path)
|
|
58
|
+
parts = Sequel.function(:string_to_array, e, ",")
|
|
59
|
+
parts = Sequel.function(:unnest, parts)
|
|
60
|
+
sel = Webhookdb::Dbutil::MOCK_CONN.
|
|
61
|
+
from(parts.as(:parts)).
|
|
62
|
+
select(Sequel.function(:trim, :parts))
|
|
63
|
+
f = Sequel.function(:array, sel)
|
|
64
|
+
return f
|
|
65
|
+
end,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Return a converter that parses a value using the given regex,
|
|
69
|
+
# and returns the capture group at index.
|
|
70
|
+
# The 'coerce' function can be applied to, for example,
|
|
71
|
+
# capture a number from a request path and store it as an integer.
|
|
72
|
+
#
|
|
73
|
+
# @param pattern [String]
|
|
74
|
+
# @param dbtype [Symbol] The DB type to use, like INTEGER or BIGINT.
|
|
75
|
+
#
|
|
76
|
+
# @note Only the first capture group can be extracted at this time.
|
|
77
|
+
def self.converter_from_regex(pattern, dbtype: nil)
|
|
78
|
+
re = self._assert_regex_converter_type(pattern)
|
|
79
|
+
case dbtype
|
|
80
|
+
when INTEGER
|
|
81
|
+
rcoerce = :to_i
|
|
82
|
+
pgcast = :integer
|
|
83
|
+
when BIGINT
|
|
84
|
+
rcoerce = :to_i
|
|
85
|
+
pgcast = :bigint
|
|
86
|
+
when nil
|
|
87
|
+
rcoerce = nil
|
|
88
|
+
pgcast = nil
|
|
89
|
+
else
|
|
90
|
+
raise NotImplementedError, "unhandled converter_from_regex dbtype: #{dbtype}"
|
|
91
|
+
end
|
|
92
|
+
return IsomorphicProc.new(
|
|
93
|
+
ruby: lambda do |value, **_|
|
|
94
|
+
matched = value&.match(re) do |md|
|
|
95
|
+
md.captures ? md.captures[0] : nil
|
|
96
|
+
end
|
|
97
|
+
(matched = matched.send(rcoerce)) if !matched.nil? && rcoerce
|
|
98
|
+
matched
|
|
99
|
+
end,
|
|
100
|
+
sql: lambda do |e|
|
|
101
|
+
f = Sequel.function(:substring, e.cast(:text), pattern)
|
|
102
|
+
f = f.cast(pgcast) if pgcast
|
|
103
|
+
f
|
|
104
|
+
end,
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Extract a number from a string using the given regexp.
|
|
109
|
+
# If nothing can be extracted, get the next value from the sequence.
|
|
110
|
+
#
|
|
111
|
+
# Note this requires `requires_sequence=true` on the replicator.
|
|
112
|
+
#
|
|
113
|
+
# Used primarily where the ID is sent by an API only in the request URL (not a key in the body),
|
|
114
|
+
# and the URL will not include an ID when it's being sent for the first time.
|
|
115
|
+
# We see this in channel manager APIs primarily, that replicate their data to 3rd parties.
|
|
116
|
+
#
|
|
117
|
+
# @note This converter does not work for backfilling/UPDATE of existing columns.
|
|
118
|
+
# It is generally only of use for unique ids.
|
|
119
|
+
def self.converter_int_or_sequence_from_regex(re, dbtype: BIGINT)
|
|
120
|
+
return Webhookdb::Replicator::Column::IsomorphicProc.new(
|
|
121
|
+
ruby: lambda do |value, service_integration:, **kw|
|
|
122
|
+
url_id = Webhookdb::Replicator::Column.converter_from_regex(re, dbtype:).
|
|
123
|
+
ruby.call(value, service_integration:, **kw)
|
|
124
|
+
url_id || service_integration.sequence_nextval
|
|
125
|
+
end,
|
|
126
|
+
sql: NOT_IMPLEMENTED,
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Parse the value in the column using the given strptime string.
|
|
131
|
+
#
|
|
132
|
+
# To provide an `sql` proc, provide the sqlformat string, which is used in TO_TIMESTAMP(col, sqlformat).
|
|
133
|
+
# Note that TO_TIMESTAMP does not support timezone offsets,
|
|
134
|
+
# so the time will always be in UTC.
|
|
135
|
+
#
|
|
136
|
+
# Future note: We may want to derive sqlformat from format,
|
|
137
|
+
# and handle timezone offsets in the timestamp strings.
|
|
138
|
+
def self.converter_strptime(format, sqlformat=nil, cls: Time)
|
|
139
|
+
return Webhookdb::Replicator::Column::IsomorphicProc.new(
|
|
140
|
+
ruby: lambda do |value, **|
|
|
141
|
+
value.nil? ? nil : cls.strptime(value, format)
|
|
142
|
+
end,
|
|
143
|
+
sql: lambda do |e|
|
|
144
|
+
raise NotImplementedError if sqlformat.nil?
|
|
145
|
+
f = Sequel.function(:to_timestamp, e, sqlformat)
|
|
146
|
+
f = f.cast(:date) if cls == Date
|
|
147
|
+
f
|
|
148
|
+
end,
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.converter_gsub(pattern, replacement)
|
|
153
|
+
re = self._assert_regex_converter_type(pattern)
|
|
154
|
+
return Webhookdb::Replicator::Column::IsomorphicProc.new(
|
|
155
|
+
ruby: lambda do |value, **|
|
|
156
|
+
value&.gsub(re, replacement)
|
|
157
|
+
end,
|
|
158
|
+
sql: lambda do |e|
|
|
159
|
+
Sequel.function(:regexp_replace, e, pattern, replacement, "g")
|
|
160
|
+
end,
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.converter_array_element(index:, sep:, cls: DECIMAL)
|
|
165
|
+
case cls
|
|
166
|
+
when DECIMAL
|
|
167
|
+
to_ruby = ->(v) { BigDecimal(v) }
|
|
168
|
+
to_sql = ->(e) { Sequel.cast(e, :decimal) }
|
|
169
|
+
else
|
|
170
|
+
raise ArgumentError, "Unsupported cls" unless valid_cls.include?(cls)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return IsomorphicProc.new(
|
|
174
|
+
ruby: lambda do |value, **|
|
|
175
|
+
break nil if value.nil?
|
|
176
|
+
parts = value.split(sep)
|
|
177
|
+
break nil if index >= parts.size
|
|
178
|
+
to_ruby.call(parts[index])
|
|
179
|
+
end,
|
|
180
|
+
sql: lambda do |expr|
|
|
181
|
+
# The expression may be a JSONB field, of the type jsonb (accessed with -> rather than ->>).
|
|
182
|
+
# Make sure it's text. The CAST will turn 'a' into '"a"' though, so we also need to trim quotes.
|
|
183
|
+
str_expr = Sequel.cast(expr, :text)
|
|
184
|
+
str_expr = Sequel.function(:btrim, str_expr, '"')
|
|
185
|
+
field_expr = Sequel.function(:split_part, str_expr, sep, index + 1)
|
|
186
|
+
# If the field is invalid, we get ''. Use nil in this case.
|
|
187
|
+
case_expr = Sequel.case({Sequel[field_expr => ""] => nil}, field_expr)
|
|
188
|
+
to_sql.call(case_expr)
|
|
189
|
+
end,
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.converter_array_pluck(key, coltype)
|
|
194
|
+
pgtype = Webhookdb::DBAdapter::PG::COLTYPE_MAP.fetch(coltype)
|
|
195
|
+
return IsomorphicProc.new(
|
|
196
|
+
ruby: lambda do |value, **|
|
|
197
|
+
break nil if value.nil?
|
|
198
|
+
break nil unless value.respond_to?(:to_ary)
|
|
199
|
+
value.map { |v| v[key] }
|
|
200
|
+
end,
|
|
201
|
+
sql: lambda do |expr|
|
|
202
|
+
expr = Sequel.lit("'#{JSON.generate(expr)}'::jsonb") if expr.is_a?(Hash) || expr.is_a?(Array)
|
|
203
|
+
Webhookdb::Dbutil::MOCK_CONN.
|
|
204
|
+
from(Sequel.function(:jsonb_to_recordset, expr).as(Sequel.lit("x(#{key} #{pgtype})"))).
|
|
205
|
+
select(Sequel.function(:array_agg, Sequel.lit(key)))
|
|
206
|
+
end,
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
DAYS_OF_WEEK = [
|
|
211
|
+
"SUNDAY",
|
|
212
|
+
"MONDAY",
|
|
213
|
+
"TUESDAY",
|
|
214
|
+
"WEDNESDAY",
|
|
215
|
+
"THURSDAY",
|
|
216
|
+
"FRIDAY",
|
|
217
|
+
"SATURDAY",
|
|
218
|
+
].freeze
|
|
219
|
+
|
|
220
|
+
# Convert a value or array by looking up its value in a map.
|
|
221
|
+
# @param array [Boolean] If true, the empty value is an array. If false, nil.
|
|
222
|
+
# @param map [Hash]
|
|
223
|
+
def self.converter_map_lookup(array:, map:)
|
|
224
|
+
empty = array ? Sequel.pg_array([]) : nil
|
|
225
|
+
return IsomorphicProc.new(
|
|
226
|
+
ruby: lambda do |value, **|
|
|
227
|
+
break empty if value.nil?
|
|
228
|
+
is_ary = value.respond_to?(:to_ary)
|
|
229
|
+
r = (is_ary ? value : [value]).map do |v|
|
|
230
|
+
if (mapval = map[v])
|
|
231
|
+
mapval
|
|
232
|
+
else
|
|
233
|
+
v
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
break is_ary ? r : r[0]
|
|
237
|
+
end,
|
|
238
|
+
sql: NOT_IMPLEMENTED,
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
KNOWN_CONVERTERS = {
|
|
243
|
+
date: CONV_PARSE_DATE,
|
|
244
|
+
time: CONV_PARSE_TIME,
|
|
245
|
+
to_i: CONV_TO_I,
|
|
246
|
+
tsat: CONV_UNIX_TS,
|
|
247
|
+
}.freeze
|
|
248
|
+
|
|
249
|
+
DEFAULTER_NOW = IsomorphicProc.new(ruby: ->(*) { Time.now }, sql: ->(*) { Sequel.function(:now) })
|
|
250
|
+
DEFAULTER_FALSE = IsomorphicProc.new(ruby: ->(*) { false }, sql: ->(*) { false })
|
|
251
|
+
DEFAULTER_FROM_INTEGRATION_SEQUENCE = IsomorphicProc.new(
|
|
252
|
+
ruby: ->(service_integration:, **_) { service_integration.sequence_nextval },
|
|
253
|
+
sql: ->(service_integration:) { Sequel.function(:nextval, service_integration.sequence_name) },
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def self.defaulter_from_resource_field(key)
|
|
257
|
+
return Webhookdb::Replicator::Column::IsomorphicProc.new(
|
|
258
|
+
ruby: ->(resource:, **_) { resource.fetch(key.to_s) },
|
|
259
|
+
sql: ->(*) { key.to_sym },
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
KNOWN_DEFAULTERS = {now: DEFAULTER_NOW, tofalse: DEFAULTER_FALSE}.freeze
|
|
263
|
+
|
|
264
|
+
# Use in data_key when a value is an array, and you want to map a value from the array.
|
|
265
|
+
EACH_ITEM = :_each_item
|
|
266
|
+
|
|
267
|
+
# @return [Symbol]
|
|
268
|
+
attr_reader :name
|
|
269
|
+
# @return [Symbol]
|
|
270
|
+
attr_reader :type
|
|
271
|
+
# @return [Boolean]
|
|
272
|
+
attr_reader :index
|
|
273
|
+
alias index? index
|
|
274
|
+
|
|
275
|
+
# True if thie index should be a partial index, using WHERE (col IS NOT NULL).
|
|
276
|
+
# The #index attribute must be true.
|
|
277
|
+
# @return [Boolean]
|
|
278
|
+
attr_reader :index_not_null
|
|
279
|
+
|
|
280
|
+
# While :name, :type, and :index are pretty standard attributes for defining a database column,
|
|
281
|
+
# the rest of these attributes are specialized to WebhookDB and deal with how we are finding
|
|
282
|
+
# and interpreting the values given to us by external services.
|
|
283
|
+
|
|
284
|
+
# `data_key` is the key we look for in the resource object. If this value is an array we will
|
|
285
|
+
# `_dig` through the object using each key successively. `data_key` defaults to the string version
|
|
286
|
+
# of whatever name you provide for the column.
|
|
287
|
+
# @return [String,Array<String>]
|
|
288
|
+
attr_reader :data_key
|
|
289
|
+
|
|
290
|
+
# `event_key` is the key we look for in the event object. This defaults to nil, but note that if
|
|
291
|
+
# both an event object and event key are provided, we will always grab the value from the event object
|
|
292
|
+
# instead of from the resource object using the `data_key`. If this value is an array we will
|
|
293
|
+
# `_dig` through the object using each key successively, same as with `data_key`.
|
|
294
|
+
# @return [String,Array<String>]
|
|
295
|
+
attr_reader :event_key
|
|
296
|
+
|
|
297
|
+
# If `from_enrichment` is set then we use the `data_key` value to find the desired value in the
|
|
298
|
+
# enrichment object. In this case, if the enrichment object is nil you will get an error.
|
|
299
|
+
# @return [Boolean]
|
|
300
|
+
attr_reader :from_enrichment
|
|
301
|
+
|
|
302
|
+
# If `optional` is true then the column will be populated with a nil value instead of throwing an error
|
|
303
|
+
# if the desired value is not present in the object you're `_dig`ging into, which could be any of the
|
|
304
|
+
# three (resource, event, and enrichment) according to the way the rest of the attributes are configured.
|
|
305
|
+
# Note that for nested values, `_dig` will return nil if *any* of the keys in the provided array are
|
|
306
|
+
# missing from the object.
|
|
307
|
+
# @return [Boolean]
|
|
308
|
+
attr_reader :optional
|
|
309
|
+
|
|
310
|
+
# Sometimes we need to do some processing on the value provided by the external service so that the we
|
|
311
|
+
# get the data we want in the format we want. A common example is parsing various DateTime formats into our
|
|
312
|
+
# desired timestamp format. In these cases, we use a `converter`, which is an `IsomorphicProc` where both
|
|
313
|
+
# procs take the value retrieved from the external service and the resource object and return a value
|
|
314
|
+
# consistent with the column's type attribute.
|
|
315
|
+
#
|
|
316
|
+
# @return [IsomorphicProc]
|
|
317
|
+
# The 'ruby' proc accepts (value, resource:, event:, enrichment:, service_integration:) and returns a value.
|
|
318
|
+
# The 'sql' proc takes an expression and returns a new expression.
|
|
319
|
+
attr_reader :converter
|
|
320
|
+
|
|
321
|
+
# If the value we retrieve from the data provided by the external service is nil, we often want to use
|
|
322
|
+
# a default value instead of nil. The `defaulter` is an `IsomorphicProc` where both procs take the resource
|
|
323
|
+
# object and return a default value that is used in the upsert. A common example is the `now` defaulter,
|
|
324
|
+
# which uses the current time as the default value.
|
|
325
|
+
#
|
|
326
|
+
# @return [IsomorphicProc]
|
|
327
|
+
# The 'ruby' proc accepts (resource:, event:, enrichment:, service_integration:) and returns a value.
|
|
328
|
+
# The 'sql' proc accepts (service_integration:) and returns an sql expression.
|
|
329
|
+
attr_reader :defaulter
|
|
330
|
+
|
|
331
|
+
# If `skip_nil` is set to true, we only add the described value to the hash that gets upserted if it is not
|
|
332
|
+
# nil. This is so that we don't override existing data in the database row with a nil value.
|
|
333
|
+
# @return [Boolean]
|
|
334
|
+
attr_reader :skip_nil
|
|
335
|
+
alias skip_nil? skip_nil
|
|
336
|
+
|
|
337
|
+
# If provided, run this before backfilling as part of UPDATE.
|
|
338
|
+
# Usually used to add functions into pg_temp schema.
|
|
339
|
+
# This is an advanced use case; see unit tests for examples.
|
|
340
|
+
attr_reader :backfill_statement
|
|
341
|
+
|
|
342
|
+
# If provided, use this expression as the UPDATE value when adding the column
|
|
343
|
+
# to an existing table.
|
|
344
|
+
# @return [String,Sequel,Sequel::SQL::Expression]
|
|
345
|
+
attr_reader :backfill_expr
|
|
346
|
+
|
|
347
|
+
def initialize(
|
|
348
|
+
name,
|
|
349
|
+
type,
|
|
350
|
+
data_key: nil,
|
|
351
|
+
event_key: nil,
|
|
352
|
+
from_enrichment: false,
|
|
353
|
+
optional: false,
|
|
354
|
+
converter: nil,
|
|
355
|
+
defaulter: nil,
|
|
356
|
+
index: false,
|
|
357
|
+
index_not_null: false,
|
|
358
|
+
skip_nil: false,
|
|
359
|
+
backfill_statement: nil,
|
|
360
|
+
backfill_expr: nil
|
|
361
|
+
)
|
|
362
|
+
raise ArgumentError, "name must be a symbol" unless name.is_a?(Symbol)
|
|
363
|
+
raise ArgumentError, "type #{type.inspect} is not supported" unless COLUMN_TYPES.include?(type)
|
|
364
|
+
raise ArgumentError, "use :tofalse as the defaulter (or nil for no defaulter)" if defaulter == false
|
|
365
|
+
@name = name
|
|
366
|
+
@type = type
|
|
367
|
+
@data_key = data_key || name.to_s
|
|
368
|
+
@event_key = event_key
|
|
369
|
+
@from_enrichment = from_enrichment
|
|
370
|
+
@optional = optional
|
|
371
|
+
@converter = KNOWN_CONVERTERS[converter] || converter
|
|
372
|
+
@defaulter = KNOWN_DEFAULTERS[defaulter] || defaulter
|
|
373
|
+
@index = index
|
|
374
|
+
@index_not_null = index_not_null
|
|
375
|
+
@skip_nil = skip_nil
|
|
376
|
+
@backfill_statement = backfill_statement
|
|
377
|
+
@backfill_expr = backfill_expr
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def to_dbadapter(**more)
|
|
381
|
+
kw = {name:, type:, index:, backfill_expr:, backfill_statement:}
|
|
382
|
+
kw[:index_where] = Sequel[self.name] !~ nil if self.index_not_null
|
|
383
|
+
kw.merge!(more)
|
|
384
|
+
return Webhookdb::DBAdapter::Column.new(**kw)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Convert this column to an expression that can be used to return
|
|
388
|
+
# the column's value based on what is present in the row.
|
|
389
|
+
# This is generally used to 'backfill' column values
|
|
390
|
+
# from what is in the data and enrichment columns.
|
|
391
|
+
#
|
|
392
|
+
# NOTE: this method assumes Postgres as the backing database.
|
|
393
|
+
# To support others will require additional work and some abstraction.
|
|
394
|
+
def to_sql_expr
|
|
395
|
+
source_col = @from_enrichment ? :enrichment : :data
|
|
396
|
+
source_col_expr = Sequel.pg_json(source_col)
|
|
397
|
+
# Have to use string keys here, PG handles it alright though.
|
|
398
|
+
dkey = @data_key.respond_to?(:to_ary) ? @data_key.map(&:to_s) : @data_key
|
|
399
|
+
expr = case self.type
|
|
400
|
+
# If we're pulling out a normal value from JSON, get it as a 'native' value (not jsonb) (ie, ->> op).
|
|
401
|
+
when TIMESTAMP, DATE, TEXT, INTEGER, BIGINT
|
|
402
|
+
source_col_expr.get_text(dkey)
|
|
403
|
+
else
|
|
404
|
+
# If this is a more complex value, get it as jsonb (ie, -> op).
|
|
405
|
+
# Note that this can be changed by the sql converter.
|
|
406
|
+
source_col_expr[Array(dkey)]
|
|
407
|
+
end
|
|
408
|
+
if self.converter
|
|
409
|
+
if self.converter.sql == NOT_IMPLEMENTED
|
|
410
|
+
msg = "Converter SQL for #{self.name} is not implemented. This column cannot be added after the fact, " \
|
|
411
|
+
"backfill_expr should be set on the column to provide a manual UPDATE/backfill converter, " \
|
|
412
|
+
"or the :sql converter can be implemented (may not be possible or feasible in all cases)."
|
|
413
|
+
raise TypeError, msg
|
|
414
|
+
end
|
|
415
|
+
conv_kwargs = self.converter.sql.arity == 1 ? {} : {json_path: dkey, source_col: source_col_expr}
|
|
416
|
+
expr = self.converter.sql.call(expr, **conv_kwargs)
|
|
417
|
+
end
|
|
418
|
+
pgcol = Webhookdb::DBAdapter::PG::COLTYPE_MAP.fetch(self.type)
|
|
419
|
+
expr = expr.cast(pgcol)
|
|
420
|
+
(expr = Sequel.function(:coalesce, expr, self.defaulter.sql.call)) if self.defaulter
|
|
421
|
+
return expr
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def to_ruby_value(resource:, event:, enrichment:, service_integration:)
|
|
425
|
+
v = if self.from_enrichment
|
|
426
|
+
self._dig(enrichment, self.data_key, self.optional)
|
|
427
|
+
elsif event && self.event_key
|
|
428
|
+
# Event keys are never optional since any API using them is going to have fixed keys
|
|
429
|
+
self._dig(event, self.event_key, false)
|
|
430
|
+
else
|
|
431
|
+
self._dig(resource, self.data_key, self.optional)
|
|
432
|
+
end
|
|
433
|
+
(v = self.defaulter.ruby.call(resource:, event:, enrichment:, service_integration:)) if self.defaulter && v.nil?
|
|
434
|
+
v = self.converter.ruby.call(v, resource:, event:, enrichment:, service_integration:) if self.converter
|
|
435
|
+
if (self.type == INTEGER_ARRAY) && !v.nil?
|
|
436
|
+
v = Sequel.pg_array(v, "integer")
|
|
437
|
+
elsif (self.type == TEXT_ARRAY) && !v.nil?
|
|
438
|
+
v = Sequel.pg_array(v, "text")
|
|
439
|
+
elsif (self.type == BIGINT_ARRAY) && !v.nil?
|
|
440
|
+
v = Sequel.pg_array(v, "bigint")
|
|
441
|
+
elsif (self.type == TIMESTAMP) && !v.nil?
|
|
442
|
+
# Postgres CANNOT handle timestamps with a 0000 year,
|
|
443
|
+
# even if the actual time it represents is valid (due to timezone offset).
|
|
444
|
+
# Repro with `SELECT '0000-12-31T18:10:00-05:50'::timestamptz`.
|
|
445
|
+
# So if we are in the year 0, represent the time into UTC to get it out of year 0
|
|
446
|
+
# (if it's still invalid, let it error).
|
|
447
|
+
# NOTE: Only worry about times; if the value is a string, it will still error.
|
|
448
|
+
# Let the caller parse the string into a Time to get this special behavior.
|
|
449
|
+
# Time parsing is too loose to do it here.
|
|
450
|
+
v = v.utc if v.is_a?(Time) && v.year.zero?
|
|
451
|
+
end
|
|
452
|
+
# pg_json doesn't handle thie ssuper well in our situation,
|
|
453
|
+
# so JSON must be inserted as a string.
|
|
454
|
+
if (_stringify_json = self.type == OBJECT && !v.nil? && !v.is_a?(String))
|
|
455
|
+
v = v.to_json
|
|
456
|
+
end
|
|
457
|
+
return v
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def _dig(h, keys, optional)
|
|
461
|
+
v = h
|
|
462
|
+
karr = Array(keys)
|
|
463
|
+
karr.each do |key|
|
|
464
|
+
begin
|
|
465
|
+
v = optional ? v[key] : v.fetch(key)
|
|
466
|
+
rescue KeyError
|
|
467
|
+
raise KeyError, "key not found: '#{key}' in: #{v.keys}"
|
|
468
|
+
rescue NoMethodError => e
|
|
469
|
+
raise NoMethodError, "Element #{key} of #{karr}\n#{e}"
|
|
470
|
+
end
|
|
471
|
+
# allow optional nested values by returning nil as soon as key not found
|
|
472
|
+
# the problem here is that you effectively set all keys in the sequence as optional
|
|
473
|
+
break if optional && v.nil?
|
|
474
|
+
end
|
|
475
|
+
return v
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def self._assert_regex_converter_type(re)
|
|
479
|
+
return Regexp.new(re) if re.is_a?(String)
|
|
480
|
+
raise ArgumentError, "regexp must be a string, not a Ruby regex, so it can be used in the database verbatim"
|
|
481
|
+
end
|
|
482
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "webhookdb/convertkit"
|
|
5
|
+
require "webhookdb/replicator/convertkit_v1_mixin"
|
|
6
|
+
|
|
7
|
+
class Webhookdb::Replicator::ConvertkitBroadcastV1 < Webhookdb::Replicator::Base
|
|
8
|
+
include Appydays::Loggable
|
|
9
|
+
include Webhookdb::Replicator::ConvertkitV1Mixin
|
|
10
|
+
|
|
11
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
12
|
+
def self.descriptor
|
|
13
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
14
|
+
name: "convertkit_broadcast_v1",
|
|
15
|
+
ctor: ->(sint) { Webhookdb::Replicator::ConvertkitBroadcastV1.new(sint) },
|
|
16
|
+
feature_roles: [],
|
|
17
|
+
resource_name_singular: "ConvertKit Broadcast",
|
|
18
|
+
supports_backfill: true,
|
|
19
|
+
api_docs_url: "https://developers.convertkit.com/#list-broadcasts",
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def _denormalized_columns
|
|
24
|
+
return [
|
|
25
|
+
Webhookdb::Replicator::Column.new(:click_rate, DECIMAL, from_enrichment: true, optional: true),
|
|
26
|
+
Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, index: true),
|
|
27
|
+
Webhookdb::Replicator::Column.new(:open_rate, DECIMAL, from_enrichment: true, optional: true),
|
|
28
|
+
Webhookdb::Replicator::Column.new(:progress, DECIMAL, from_enrichment: true, optional: true),
|
|
29
|
+
Webhookdb::Replicator::Column.new(:recipients, INTEGER, from_enrichment: true, optional: true),
|
|
30
|
+
Webhookdb::Replicator::Column.new(:show_total_clicks, BOOLEAN, from_enrichment: true, optional: true),
|
|
31
|
+
Webhookdb::Replicator::Column.new(:status, TEXT, from_enrichment: true, optional: true),
|
|
32
|
+
Webhookdb::Replicator::Column.new(:subject, TEXT),
|
|
33
|
+
Webhookdb::Replicator::Column.new(:total_clicks, INTEGER, from_enrichment: true, optional: true),
|
|
34
|
+
Webhookdb::Replicator::Column.new(:unsubscribes, INTEGER, from_enrichment: true, optional: true),
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def _resource_and_event(request)
|
|
39
|
+
return request.body, nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def _timestamp_column_name
|
|
43
|
+
return :created_at
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def _update_where_expr
|
|
47
|
+
return self.qualified_table_sequel_identifier[:data] !~ Sequel[:excluded][:data]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def _store_enrichment_body?
|
|
51
|
+
return true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def _fetch_enrichment(resource, _event, _request)
|
|
55
|
+
broadcast_id = resource.fetch("id")
|
|
56
|
+
url = "https://api.convertkit.com/v3/broadcasts/#{broadcast_id}/stats?api_secret=#{self.service_integration.backfill_secret}"
|
|
57
|
+
response = Webhookdb::Http.get(url, logger: self.logger, timeout: Webhookdb::Convertkit.http_timeout)
|
|
58
|
+
data = response.parsed_response
|
|
59
|
+
return data.dig("broadcast", "stats") || {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def _fetch_backfill_page(_pagination_token, **_kwargs)
|
|
63
|
+
# this endpoint does not have pagination support
|
|
64
|
+
url = "https://api.convertkit.com/v3/broadcasts?api_secret=#{self.service_integration.backfill_secret}"
|
|
65
|
+
response = Webhookdb::Http.get(url, logger: self.logger, timeout: Webhookdb::Convertkit.http_timeout)
|
|
66
|
+
data = response.parsed_response
|
|
67
|
+
return data["broadcasts"], nil
|
|
68
|
+
end
|
|
69
|
+
end
|