From 39f221744d8fb0eb2e59023bd8b75758e5c36fcf Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Wed, 13 Aug 2025 10:31:17 +1200 Subject: [PATCH 01/37] chore: Add desc to tasks so they display in task list --- lib/consyncful/tasks/consyncful.rake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/consyncful/tasks/consyncful.rake b/lib/consyncful/tasks/consyncful.rake index 31fb6ff..89d37f6 100644 --- a/lib/consyncful/tasks/consyncful.rake +++ b/lib/consyncful/tasks/consyncful.rake @@ -1,14 +1,17 @@ # frozen_string_literal: true namespace :consyncful do + desc "Run a one-time sync of the latest Contentful data into the app" task update: [:environment] do Consyncful::Sync.latest.run end + desc "Run a one-time full refresh of all Contentful data into the app (bypasses caching)" task refresh: [:environment] do Consyncful::Sync.fresh.run end + desc "Continuously sync the latest Contentful data every N seconds (default: 15). Usage: rake consyncful:sync[SECONDS]" task :sync, [:seconds] => %i[environment update_model_names] do |_task, args| Signal.trap('TERM') do puts Rainbow("Graceful shutdown PID=#{Process.pid}").red @@ -23,6 +26,7 @@ namespace :consyncful do end end + desc "Update stored model _type fields based on Contentful type mappings" task update_model_names: [:environment] do if Rails.autoloaders.zeitwerk_enabled? Zeitwerk::Loader.eager_load_all From 881066ae0b0696e37021026a2b48e58f4dd3a82f Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Wed, 13 Aug 2025 11:35:57 +1200 Subject: [PATCH 02/37] feat: Setup initial webhook controller action --- .../consyncful/webhook_controller.rb | 9 ++++++ config/routes.rb | 3 ++ lib/consyncful.rb | 12 ++++++-- lib/consyncful/config/debounce.rb | 11 ++++++++ lib/consyncful/config/jobs.rb | 11 ++++++++ lib/consyncful/config/lock.rb | 12 ++++++++ lib/consyncful/config/webhook.rb | 16 +++++++++++ lib/consyncful/configuration.rb | 28 ++++++++++++++++++- lib/consyncful/engine.rb | 7 +++++ 9 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 app/controllers/consyncful/webhook_controller.rb create mode 100644 config/routes.rb create mode 100644 lib/consyncful/config/debounce.rb create mode 100644 lib/consyncful/config/jobs.rb create mode 100644 lib/consyncful/config/lock.rb create mode 100644 lib/consyncful/config/webhook.rb create mode 100644 lib/consyncful/engine.rb diff --git a/app/controllers/consyncful/webhook_controller.rb b/app/controllers/consyncful/webhook_controller.rb new file mode 100644 index 0000000..2ee6c45 --- /dev/null +++ b/app/controllers/consyncful/webhook_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Consyncful + class WebhookController < ActionController::API + def create + head :accepted + end + end +end diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..6618fb3 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,3 @@ +Consyncful::Engine.routes.draw do + post '/webhook', to: 'webhook#create' +end \ No newline at end of file diff --git a/lib/consyncful.rb b/lib/consyncful.rb index 53c6793..81fd581 100644 --- a/lib/consyncful.rb +++ b/lib/consyncful.rb @@ -1,9 +1,17 @@ # frozen_string_literal: true -require 'consyncful/version' +# External dependencies require 'mongoid' require 'contentful' + +# Internal library files +require 'consyncful/version' require 'consyncful/configuration' require 'consyncful/base' require 'consyncful/sync' -require 'consyncful/railtie' if defined?(Rails) + +# Rails integration (only load if Rails is present) +if defined?(Rails) + require 'consyncful/railtie' + require 'consyncful/engine' +end diff --git a/lib/consyncful/config/debounce.rb b/lib/consyncful/config/debounce.rb new file mode 100644 index 0000000..0e2c8b9 --- /dev/null +++ b/lib/consyncful/config/debounce.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +module Consyncful + module Config + class Debounce + attr_accessor :window_seconds + def initialize + @window_seconds = 10 + end + end + end +end diff --git a/lib/consyncful/config/jobs.rb b/lib/consyncful/config/jobs.rb new file mode 100644 index 0000000..296447e --- /dev/null +++ b/lib/consyncful/config/jobs.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +module Consyncful + module Config + class Jobs + attr_accessor :queue + def initialize + @queue = :consyncful + end + end + end +end diff --git a/lib/consyncful/config/lock.rb b/lib/consyncful/config/lock.rb new file mode 100644 index 0000000..f417ea9 --- /dev/null +++ b/lib/consyncful/config/lock.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Consyncful + module Config + class Lock + attr_accessor :backend, :ttl_seconds + def initialize + @backend = :mongo + @ttl_seconds = 15 * 60 + end + end + end +end diff --git a/lib/consyncful/config/webhook.rb b/lib/consyncful/config/webhook.rb new file mode 100644 index 0000000..3569e9e --- /dev/null +++ b/lib/consyncful/config/webhook.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module Consyncful + module Config + class Webhook + attr_accessor :enabled, :path, :secret, :ttl_seconds, :accept_topics + + def initialize + @enabled = false + @path = '/consyncful/webhook' + @secret = ENV['CONSYNCFUL_WEBHOOK_SECRET'] + @ttl_seconds = 60 + @accept_topics = %w[ContentManagement.Entry.* ContentManagement.Asset.*] + end + end + end +end diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index eabc655..6c769ed 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true +require 'consyncful/config/webhook' +require 'consyncful/config/jobs' +require 'consyncful/config/debounce' +require 'consyncful/config/lock' + # Handles Rails configurations for Consyncful module Consyncful - ## class Configuration attr_accessor :contentful_client_options, :contentful_sync_options, @@ -13,6 +17,8 @@ class Configuration :ignore_content_tags, :preserve_contentful_timestamps + attr_reader :webhook, :jobs, :debounce, :lock + def initialize @contentful_client_options = {} @contentful_sync_options = {} @@ -22,6 +28,11 @@ def initialize @content_tags = [] @ignore_content_tags = [] @preserve_contentful_timestamps = false + + @webhook = Config::Webhook.new + @jobs = Config::Jobs.new + @debounce = Config::Debounce.new + @lock = Config::Lock.new end def initial_sync_options @@ -34,6 +45,21 @@ def client_options options = @contentful_client_options options.reverse_merge!(DEFAULT_CLIENT_OPTIONS) end + + def use_webhooks? + webhook.enabled && webhook.secret.present? + end + + def webhook_path; webhook.path; end + def webhook_secret; webhook.secret; end + def webhook_ttl_seconds; webhook.ttl_seconds; end + def webhook_accept_topics; webhook.accept_topics; end + + def job_queue; jobs.queue; end + def debounce_window; debounce.window_seconds; end + + def lock_backend; lock.backend; end + def lock_ttl_seconds; lock.ttl_seconds; end end DEFAULT_CLIENT_OPTIONS = { diff --git a/lib/consyncful/engine.rb b/lib/consyncful/engine.rb new file mode 100644 index 0000000..cedb76a --- /dev/null +++ b/lib/consyncful/engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Consyncful + class Engine < Rails::Engine + isolate_namespace Consyncful + end +end \ No newline at end of file From 65dae6c132cbe1c4976aa5850dbf0a04692dcd44 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Wed, 13 Aug 2025 13:53:00 +1200 Subject: [PATCH 03/37] feat: Add sync mode option to config --- lib/consyncful/configuration.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index 6c769ed..af5c85e 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -15,11 +15,13 @@ class Configuration :mongo_collection, :content_tags, :ignore_content_tags, - :preserve_contentful_timestamps + :preserve_contentful_timestamps, + :sync_mode attr_reader :webhook, :jobs, :debounce, :lock def initialize + @sync_mode = :poll @contentful_client_options = {} @contentful_sync_options = {} @locale = 'en-NZ' From 52373bb39c06c8d762911579ab22c95e7f626955 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Wed, 13 Aug 2025 14:15:51 +1200 Subject: [PATCH 04/37] feat: Add sync mode option to config --- lib/consyncful/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index af5c85e..585f374 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -49,7 +49,7 @@ def client_options end def use_webhooks? - webhook.enabled && webhook.secret.present? + sync_mode == :webhook && webhook.secret.present? end def webhook_path; webhook.path; end From b0287c41a5d9ce856aafb8be7c6e48dbbdeb9b8f Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Wed, 13 Aug 2025 14:22:45 +1200 Subject: [PATCH 05/37] feat: Add controller to mount in app to use webhooks for sync --- .../consyncful/webhook_controller.rb | 5 +++ lib/consyncful/configuration.rb | 2 +- lib/consyncful/sync.rb | 21 +++++++++++ lib/consyncful/tasks/consyncful.rake | 35 ++++++++++++++++--- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/app/controllers/consyncful/webhook_controller.rb b/app/controllers/consyncful/webhook_controller.rb index 2ee6c45..15ddc59 100644 --- a/app/controllers/consyncful/webhook_controller.rb +++ b/app/controllers/consyncful/webhook_controller.rb @@ -3,6 +3,11 @@ module Consyncful class WebhookController < ActionController::API def create + return head :not_found unless Consyncful.configuration.use_webhooks? + + # TODO: Verify the request + + Consyncful::Sync.signal_webhook! head :accepted end end diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index 585f374..2b3eea1 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -49,7 +49,7 @@ def client_options end def use_webhooks? - sync_mode == :webhook && webhook.secret.present? + self.sync_mode == :webhook # TODO: Add webhook secret check end def webhook_path; webhook.path; end diff --git a/lib/consyncful/sync.rb b/lib/consyncful/sync.rb index fc4cfea..041bc69 100644 --- a/lib/consyncful/sync.rb +++ b/lib/consyncful/sync.rb @@ -27,10 +27,31 @@ class Sync field :next_url field :last_run_at, type: DateTime + field :webhook_pending, type: Boolean, default: false + def self.latest last || new end + ## + # Signal that a webhook has been received and a sync should be triggered + def self.signal_webhook! + doc = latest + # Upsert so it works even if no Sync doc exists yet; atomic set to true + collection.find({_id: doc.id}) + .find_one_and_update({'$set' => {'webhook_pending' => true}}, upsert: true) + true + end + + ## + # Consume the webhook signal and set webhook_pending to false + def self.consume_webhook_signal! + doc = latest + res = collection.find({_id: doc.id, 'webhook_pending' => true}) + .find_one_and_update({'$set' => {'webhook_pending' => false}}) + res + end + ## # Delete the previous sync chains from database and create a fresh one. # Used to completely resync all items from Contentful. diff --git a/lib/consyncful/tasks/consyncful.rake b/lib/consyncful/tasks/consyncful.rake index 89d37f6..ba20f0e 100644 --- a/lib/consyncful/tasks/consyncful.rake +++ b/lib/consyncful/tasks/consyncful.rake @@ -11,22 +11,47 @@ namespace :consyncful do Consyncful::Sync.fresh.run end - desc "Continuously sync the latest Contentful data every N seconds (default: 15). Usage: rake consyncful:sync[SECONDS]" + desc "Continuously sync Contentful data. Default: poll every N seconds (default: 15)." \ + "Usage: rake consyncful:sync[SECONDS] | Modes: poll (default) or webhook" task :sync, [:seconds] => %i[environment update_model_names] do |_task, args| Signal.trap('TERM') do puts Rainbow("Graceful shutdown PID=#{Process.pid}").red exit 0 end + # interval: in poll mode it's the poll interval; in webhook mode it's the check interval seconds = args[:seconds].to_i seconds = 15 if seconds.zero? - loop do - Consyncful::Sync.latest.run - sleep(seconds) + + # mode comes from config (preferred) or ENV fallback; default :poll + config = Consyncful.respond_to?(:configuration) ? Consyncful.configuration : nil + mode = (config&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll).to_sym + + case mode + when :poll + puts "[consyncful] mode=:poll interval=#{seconds}s" + loop do + Consyncful::Sync.latest.run + sleep(seconds) + end + when :webhook + puts "[consyncful] mode=:webhook check_every=#{seconds}s" + loop do + # Only run when a webhook has set the boolean; consume it atomically + if Consyncful::Sync.consume_webhook_signal! + Consyncful::Sync.latest.run + else + sleep(seconds) + end + end + + else + raise "Unknown sync mode: #{mode.inspect} (expected :poll or :webhook)" end end - desc "Update stored model _type fields based on Contentful type mappings" + + desc "Update stored model_type fields based on Contentful type mappings" task update_model_names: [:environment] do if Rails.autoloaders.zeitwerk_enabled? Zeitwerk::Loader.eager_load_all From 71f367e8149723e10094ec0f02cd994e5f22f5c5 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Wed, 13 Aug 2025 16:19:37 +1200 Subject: [PATCH 06/37] feat: SyncRunner object to encapsulate sync logic --- lib/consyncful/sync_runner.rb | 83 ++++++++++++++++++++++++++++ lib/consyncful/tasks/consyncful.rake | 44 ++++----------- 2 files changed, 93 insertions(+), 34 deletions(-) create mode 100644 lib/consyncful/sync_runner.rb diff --git a/lib/consyncful/sync_runner.rb b/lib/consyncful/sync_runner.rb new file mode 100644 index 0000000..63939c4 --- /dev/null +++ b/lib/consyncful/sync_runner.rb @@ -0,0 +1,83 @@ +# lib/consyncful/sync_runner.rb +module Consyncful + class SyncRunner + DEFAULT_INTERVAL = 15 + VALID_MODES = %i[poll webhook].freeze + + def initialize(seconds: nil, mode: nil, logger: nil) + @interval = seconds || DEFAULT_INTERVAL + @mode = normalize_mode(mode || resolved_mode_from_config_or_env) + @logger = logger || Logger.new($stdout).tap { |l| l.progname = "consyncful" } + @sync = Consyncful::Sync.latest + @stop = false + @shutdown_reason = nil + end + + def run + trap_signals! + log "mode=#{@mode.inspect} interval=#{@interval}s" + + case @mode + when :poll then run_poll + when :webhook then run_webhook + end + ensure + if @shutdown_reason + log "Graceful shutdown (#{@shutdown_reason}) PID=#{Process.pid}", level: :warn + end + end + + private + + def run_poll + loop do + break if @stop + @sync.run + sleep(@interval) + end + end + + def run_webhook + @sync.run + loop do + break if @stop + if Consyncful::Sync.consume_webhook_signal! + @sync.run + else + sleep(@interval) + end + end + end + + def normalize_mode(value) + sym = value.to_sym + return sym if VALID_MODES.include?(sym) + raise ArgumentError, "Unknown sync mode: #{sym.inspect} (expected :poll or :webhook)" + end + + def resolved_mode_from_config_or_env + if Consyncful.respond_to?(:configuration) + Consyncful.configuration&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll + else + ENV['CONSYNCFUL_SYNC_MODE'] || :poll + end + end + + def trap_signals! + %w[TERM INT].each do |sig| + Signal.trap(sig) do + @shutdown_reason ||= sig + @stop = true + end + end + end + + def log(msg, level: :info) + case level + when :warn then @logger.warn(msg) + when :error then @logger.error(msg) + else @logger.info(msg) + end + end + end +end diff --git a/lib/consyncful/tasks/consyncful.rake b/lib/consyncful/tasks/consyncful.rake index ba20f0e..df00d42 100644 --- a/lib/consyncful/tasks/consyncful.rake +++ b/lib/consyncful/tasks/consyncful.rake @@ -11,43 +11,19 @@ namespace :consyncful do Consyncful::Sync.fresh.run end - desc "Continuously sync Contentful data. Default: poll every N seconds (default: 15)." \ - "Usage: rake consyncful:sync[SECONDS] | Modes: poll (default) or webhook" + desc "Continuously sync Contentful data. Default: poll every N seconds (default: 15).\n" \ + "Usage: rake consyncful:sync[SECONDS] | Modes: poll (default) or webhook" task :sync, [:seconds] => %i[environment update_model_names] do |_task, args| - Signal.trap('TERM') do - puts Rainbow("Graceful shutdown PID=#{Process.pid}").red - exit 0 - end - - # interval: in poll mode it's the poll interval; in webhook mode it's the check interval - seconds = args[:seconds].to_i - seconds = 15 if seconds.zero? - - # mode comes from config (preferred) or ENV fallback; default :poll - config = Consyncful.respond_to?(:configuration) ? Consyncful.configuration : nil - mode = (config&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll).to_sym + require "consyncful/sync_runner" - case mode - when :poll - puts "[consyncful] mode=:poll interval=#{seconds}s" - loop do - Consyncful::Sync.latest.run - sleep(seconds) - end - when :webhook - puts "[consyncful] mode=:webhook check_every=#{seconds}s" - loop do - # Only run when a webhook has set the boolean; consume it atomically - if Consyncful::Sync.consume_webhook_signal! - Consyncful::Sync.latest.run - else - sleep(seconds) - end - end + seconds = args[:seconds] + mode = if Consyncful.respond_to?(:configuration) + Consyncful.configuration&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll + else + ENV['CONSYNCFUL_SYNC_MODE'] || :poll + end - else - raise "Unknown sync mode: #{mode.inspect} (expected :poll or :webhook)" - end + Consyncful::SyncRunner.new(seconds: seconds, mode: mode).run end From 39158433c29f414a2322bf9aab1224d6313c73ce Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Wed, 13 Aug 2025 16:19:54 +1200 Subject: [PATCH 07/37] refactor: Removed unneeded config --- lib/consyncful/config/debounce.rb | 11 ----------- lib/consyncful/config/jobs.rb | 11 ----------- lib/consyncful/config/lock.rb | 12 ------------ lib/consyncful/configuration.rb | 25 +------------------------ 4 files changed, 1 insertion(+), 58 deletions(-) delete mode 100644 lib/consyncful/config/debounce.rb delete mode 100644 lib/consyncful/config/jobs.rb delete mode 100644 lib/consyncful/config/lock.rb diff --git a/lib/consyncful/config/debounce.rb b/lib/consyncful/config/debounce.rb deleted file mode 100644 index 0e2c8b9..0000000 --- a/lib/consyncful/config/debounce.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true -module Consyncful - module Config - class Debounce - attr_accessor :window_seconds - def initialize - @window_seconds = 10 - end - end - end -end diff --git a/lib/consyncful/config/jobs.rb b/lib/consyncful/config/jobs.rb deleted file mode 100644 index 296447e..0000000 --- a/lib/consyncful/config/jobs.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true -module Consyncful - module Config - class Jobs - attr_accessor :queue - def initialize - @queue = :consyncful - end - end - end -end diff --git a/lib/consyncful/config/lock.rb b/lib/consyncful/config/lock.rb deleted file mode 100644 index f417ea9..0000000 --- a/lib/consyncful/config/lock.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true -module Consyncful - module Config - class Lock - attr_accessor :backend, :ttl_seconds - def initialize - @backend = :mongo - @ttl_seconds = 15 * 60 - end - end - end -end diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index 2b3eea1..c9f79ae 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -1,10 +1,5 @@ # frozen_string_literal: true -require 'consyncful/config/webhook' -require 'consyncful/config/jobs' -require 'consyncful/config/debounce' -require 'consyncful/config/lock' - # Handles Rails configurations for Consyncful module Consyncful class Configuration @@ -18,8 +13,6 @@ class Configuration :preserve_contentful_timestamps, :sync_mode - attr_reader :webhook, :jobs, :debounce, :lock - def initialize @sync_mode = :poll @contentful_client_options = {} @@ -30,11 +23,6 @@ def initialize @content_tags = [] @ignore_content_tags = [] @preserve_contentful_timestamps = false - - @webhook = Config::Webhook.new - @jobs = Config::Jobs.new - @debounce = Config::Debounce.new - @lock = Config::Lock.new end def initial_sync_options @@ -49,19 +37,8 @@ def client_options end def use_webhooks? - self.sync_mode == :webhook # TODO: Add webhook secret check + sync_mode == :webhook # TODO: Add webhook secret check end - - def webhook_path; webhook.path; end - def webhook_secret; webhook.secret; end - def webhook_ttl_seconds; webhook.ttl_seconds; end - def webhook_accept_topics; webhook.accept_topics; end - - def job_queue; jobs.queue; end - def debounce_window; debounce.window_seconds; end - - def lock_backend; lock.backend; end - def lock_ttl_seconds; lock.ttl_seconds; end end DEFAULT_CLIENT_OPTIONS = { From d9c75aec09e52f6213d55d97730dc0ca6247b229 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 13:07:47 +1200 Subject: [PATCH 08/37] feat: Add HTTP Auth to webhook functionality --- .../consyncful/webhook_controller.rb | 18 ++++++++++++++++-- lib/consyncful/configuration.rb | 12 ++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/controllers/consyncful/webhook_controller.rb b/app/controllers/consyncful/webhook_controller.rb index 15ddc59..cc775c2 100644 --- a/app/controllers/consyncful/webhook_controller.rb +++ b/app/controllers/consyncful/webhook_controller.rb @@ -2,13 +2,27 @@ module Consyncful class WebhookController < ActionController::API + include ActionController::HttpAuthentication::Basic::ControllerMethods + before_action :authenticate, if: -> { Consyncful.configuration.use_webhooks? } + def create return head :not_found unless Consyncful.configuration.use_webhooks? - # TODO: Verify the request - Consyncful::Sync.signal_webhook! head :accepted end + + private + + def authenticate + config = Consyncful.configuration + authenticate_or_request_with_http_basic('Consyncful: Authenticate to Trigger Sync') do |user, pass| + secure_compare(user, config.resolved_webhook_user) && secure_compare(pass, config.resolved_webhook_password) + end + end + + def secure_compare(a, b) + ActiveSupport::SecurityUtils.secure_compare(a.to_s, b.to_s) + end end end diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index c9f79ae..f5855e6 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -11,7 +11,9 @@ class Configuration :content_tags, :ignore_content_tags, :preserve_contentful_timestamps, - :sync_mode + :sync_mode, + :webhook_user, + :webhook_password def initialize @sync_mode = :poll @@ -23,6 +25,9 @@ def initialize @content_tags = [] @ignore_content_tags = [] @preserve_contentful_timestamps = false + + @webhook_user = nil + @webhook_password = nil end def initial_sync_options @@ -36,8 +41,11 @@ def client_options options.reverse_merge!(DEFAULT_CLIENT_OPTIONS) end + def resolved_webhook_user = @webhook_user || ENV['CONTENTFUL_WEBHOOK_USER'] + def resolved_webhook_password = @webhook_password || ENV['CONTENTFUL_WEBHOOK_PASSWORD'] + def use_webhooks? - sync_mode == :webhook # TODO: Add webhook secret check + sync_mode == :webhook && resolved_webhook_user.present? && resolved_webhook_password.present? end end From 47a5e50c68748c5f1c2eb42ec656c327e7108d65 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 13:13:37 +1200 Subject: [PATCH 09/37] fix: Use communicative variable name --- app/controllers/consyncful/webhook_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/consyncful/webhook_controller.rb b/app/controllers/consyncful/webhook_controller.rb index cc775c2..0dae2cb 100644 --- a/app/controllers/consyncful/webhook_controller.rb +++ b/app/controllers/consyncful/webhook_controller.rb @@ -21,8 +21,8 @@ def authenticate end end - def secure_compare(a, b) - ActiveSupport::SecurityUtils.secure_compare(a.to_s, b.to_s) + def secure_compare(value, expected) + ActiveSupport::SecurityUtils.secure_compare(value.to_s, expected.to_s) end end end From e8ed9901e4137897128df67bdaaca2d7e25813a5 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 13:13:55 +1200 Subject: [PATCH 10/37] fix: Remove duplicate method call --- lib/consyncful/base.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/consyncful/base.rb b/lib/consyncful/base.rb index c60555b..2f30858 100644 --- a/lib/consyncful/base.rb +++ b/lib/consyncful/base.rb @@ -14,9 +14,10 @@ class Base client: Consyncful.configuration.mongo_client.to_s def self.contentful_model_name(name) - self.model_map ||= {} + model_map = self.model_map + model_map ||= {} - self.model_map[name] = self + model_map[name] = self end # rubocop:disable Lint/NestedMethodDefinition From 890a79e854e0d98e82abd09010f797aa6249bda7 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 13:14:42 +1200 Subject: [PATCH 11/37] fix: Remove unused class --- lib/consyncful/config/webhook.rb | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 lib/consyncful/config/webhook.rb diff --git a/lib/consyncful/config/webhook.rb b/lib/consyncful/config/webhook.rb deleted file mode 100644 index 3569e9e..0000000 --- a/lib/consyncful/config/webhook.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true -module Consyncful - module Config - class Webhook - attr_accessor :enabled, :path, :secret, :ttl_seconds, :accept_topics - - def initialize - @enabled = false - @path = '/consyncful/webhook' - @secret = ENV['CONSYNCFUL_WEBHOOK_SECRET'] - @ttl_seconds = 60 - @accept_topics = %w[ContentManagement.Entry.* ContentManagement.Asset.*] - end - end - end -end From cb76b0bf3555b745abca7c72be1197d942ab0b4d Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 13:20:34 +1200 Subject: [PATCH 12/37] fix: Remove duplicate method calls --- lib/consyncful/item_mapper.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/consyncful/item_mapper.rb b/lib/consyncful/item_mapper.rb index b4f3491..c9d09f1 100644 --- a/lib/consyncful/item_mapper.rb +++ b/lib/consyncful/item_mapper.rb @@ -14,8 +14,12 @@ def deletion? end def excluded_by_tag? - return (Consyncful.configuration.content_tags & item_tag_ids).empty? if Consyncful.configuration.content_tags.any? - return (Consyncful.configuration.ignore_content_tags & item_tag_ids).any? if Consyncful.configuration.ignore_content_tags.any? + config = Consyncful.configuration + content_tags = config.content_tags + ignore_content_tags = config.ignore_content_tags + + return (content_tags & item_tag_ids).empty? if content_tags.any? + return (ignore_content_tags & item_tag_ids).any? if ignore_content_tags.any? false end @@ -65,7 +69,7 @@ def localized_fields(default_locale) @item.fields_with_locales.each do |field, value_with_locales| value_with_locales.each do |locale_code, value| - next if value.is_a? Contentful::File # assets are handeled below + next if value.is_a? Contentful::File # assets are handled below field_name = localized_field_name(field, locale_code, default_locale) field_name, value = mapped_field_entry_for(field_name, value) From a9833ba9e3554113fe7e2c59ad70112112888c4d Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 13:38:50 +1200 Subject: [PATCH 13/37] test: Update configuration test to use new webhook options --- spec/consyncful/configuration_spec.rb | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/spec/consyncful/configuration_spec.rb b/spec/consyncful/configuration_spec.rb index b34d17a..88906db 100644 --- a/spec/consyncful/configuration_spec.rb +++ b/spec/consyncful/configuration_spec.rb @@ -5,6 +5,10 @@ RSpec.describe Consyncful::Configuration do let(:configuration) { Consyncful::Configuration.new } + it "defaults to poll sync_mode" do + expect(configuration.sync_mode).to eq(:poll) + end + describe '.initial_sync_options' do it 'always contains initial:true' do result = configuration.initial_sync_options @@ -86,4 +90,36 @@ end end end + + describe '.use_webhooks?' do + it 'is false by default' do + expect(configuration.use_webhooks?).to eq false + end + + it "is false if mode=webhook but credentials are missing" do + configuration.sync_mode = :webhook + configuration.webhook_user = nil + configuration.webhook_password = nil + expect(configuration.use_webhooks?).to eq false + end + + it "is true when mode=webhook and credentials are set on config" do + configuration.sync_mode = :webhook + configuration.webhook_user = "username" + configuration.webhook_password = "password" + expect(configuration.use_webhooks?).to eq true + end + + context "when credentials are set on ENV" do + before { allow(ENV).to receive(:[]).with('CONTENTFUL_WEBHOOK_USER').and_return('username') } + before { allow(ENV).to receive(:[]).with('CONTENTFUL_WEBHOOK_PASSWORD').and_return('password') } + + it "resolves credentials from ENV" do + configuration.sync_mode = :webhook + expect(configuration.use_webhooks?).to eq true + expect(configuration.resolved_webhook_user).to eq("username") + expect(configuration.resolved_webhook_password).to eq("password") + end + end + end end From 2f066407ee235ca501d8542c424d0b3da1702bc2 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 13:52:18 +1200 Subject: [PATCH 14/37] fix: Rubocop --- config/routes.rb | 4 +- lib/consyncful/base.rb | 1 + lib/consyncful/engine.rb | 2 +- lib/consyncful/sync.rb | 9 ++-- lib/consyncful/sync_runner.rb | 11 ++-- lib/consyncful/tasks/consyncful.rake | 19 ++++--- spec/consyncful/configuration_spec.rb | 18 +++---- .../consyncful/webhook_controller.rb | 51 +++++++++++++++++++ 8 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 spec/controllers/consyncful/webhook_controller.rb diff --git a/config/routes.rb b/config/routes.rb index 6618fb3..d023781 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Consyncful::Engine.routes.draw do post '/webhook', to: 'webhook#create' -end \ No newline at end of file +end diff --git a/lib/consyncful/base.rb b/lib/consyncful/base.rb index 2f30858..26839b1 100644 --- a/lib/consyncful/base.rb +++ b/lib/consyncful/base.rb @@ -18,6 +18,7 @@ def self.contentful_model_name(name) model_map ||= {} model_map[name] = self + model_map end # rubocop:disable Lint/NestedMethodDefinition diff --git a/lib/consyncful/engine.rb b/lib/consyncful/engine.rb index cedb76a..8a5d384 100644 --- a/lib/consyncful/engine.rb +++ b/lib/consyncful/engine.rb @@ -4,4 +4,4 @@ module Consyncful class Engine < Rails::Engine isolate_namespace Consyncful end -end \ No newline at end of file +end diff --git a/lib/consyncful/sync.rb b/lib/consyncful/sync.rb index 041bc69..c4dd264 100644 --- a/lib/consyncful/sync.rb +++ b/lib/consyncful/sync.rb @@ -38,8 +38,8 @@ def self.latest def self.signal_webhook! doc = latest # Upsert so it works even if no Sync doc exists yet; atomic set to true - collection.find({_id: doc.id}) - .find_one_and_update({'$set' => {'webhook_pending' => true}}, upsert: true) + collection.find({ _id: doc.id }) + .find_one_and_update({ '$set' => { 'webhook_pending' => true } }, upsert: true) true end @@ -47,9 +47,8 @@ def self.signal_webhook! # Consume the webhook signal and set webhook_pending to false def self.consume_webhook_signal! doc = latest - res = collection.find({_id: doc.id, 'webhook_pending' => true}) - .find_one_and_update({'$set' => {'webhook_pending' => false}}) - res + collection.find({ _id: doc.id, 'webhook_pending' => true }) + .find_one_and_update({ '$set' => { 'webhook_pending' => false } }) end ## diff --git a/lib/consyncful/sync_runner.rb b/lib/consyncful/sync_runner.rb index 63939c4..2ce7d2d 100644 --- a/lib/consyncful/sync_runner.rb +++ b/lib/consyncful/sync_runner.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # lib/consyncful/sync_runner.rb module Consyncful class SyncRunner @@ -7,7 +9,7 @@ class SyncRunner def initialize(seconds: nil, mode: nil, logger: nil) @interval = seconds || DEFAULT_INTERVAL @mode = normalize_mode(mode || resolved_mode_from_config_or_env) - @logger = logger || Logger.new($stdout).tap { |l| l.progname = "consyncful" } + @logger = logger || Logger.new($stdout).tap { |l| l.progname = 'consyncful' } @sync = Consyncful::Sync.latest @stop = false @shutdown_reason = nil @@ -22,9 +24,7 @@ def run when :webhook then run_webhook end ensure - if @shutdown_reason - log "Graceful shutdown (#{@shutdown_reason}) PID=#{Process.pid}", level: :warn - end + log "Graceful shutdown (#{@shutdown_reason}) PID=#{Process.pid}", level: :warn if @shutdown_reason end private @@ -32,6 +32,7 @@ def run def run_poll loop do break if @stop + @sync.run sleep(@interval) end @@ -41,6 +42,7 @@ def run_webhook @sync.run loop do break if @stop + if Consyncful::Sync.consume_webhook_signal! @sync.run else @@ -52,6 +54,7 @@ def run_webhook def normalize_mode(value) sym = value.to_sym return sym if VALID_MODES.include?(sym) + raise ArgumentError, "Unknown sync mode: #{sym.inspect} (expected :poll or :webhook)" end diff --git a/lib/consyncful/tasks/consyncful.rake b/lib/consyncful/tasks/consyncful.rake index df00d42..0a692c3 100644 --- a/lib/consyncful/tasks/consyncful.rake +++ b/lib/consyncful/tasks/consyncful.rake @@ -1,33 +1,32 @@ # frozen_string_literal: true namespace :consyncful do - desc "Run a one-time sync of the latest Contentful data into the app" + desc 'Run a one-time sync of the latest Contentful data into the app' task update: [:environment] do Consyncful::Sync.latest.run end - desc "Run a one-time full refresh of all Contentful data into the app (bypasses caching)" + desc 'Run a one-time full refresh of all Contentful data into the app (bypasses caching)' task refresh: [:environment] do Consyncful::Sync.fresh.run end desc "Continuously sync Contentful data. Default: poll every N seconds (default: 15).\n" \ - "Usage: rake consyncful:sync[SECONDS] | Modes: poll (default) or webhook" + 'Usage: rake consyncful:sync[SECONDS] | Modes: poll (default) or webhook' task :sync, [:seconds] => %i[environment update_model_names] do |_task, args| - require "consyncful/sync_runner" + require 'consyncful/sync_runner' seconds = args[:seconds] mode = if Consyncful.respond_to?(:configuration) - Consyncful.configuration&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll - else - ENV['CONSYNCFUL_SYNC_MODE'] || :poll - end + Consyncful.configuration&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll + else + ENV['CONSYNCFUL_SYNC_MODE'] || :poll + end Consyncful::SyncRunner.new(seconds: seconds, mode: mode).run end - - desc "Update stored model_type fields based on Contentful type mappings" + desc 'Update stored model_type fields based on Contentful type mappings' task update_model_names: [:environment] do if Rails.autoloaders.zeitwerk_enabled? Zeitwerk::Loader.eager_load_all diff --git a/spec/consyncful/configuration_spec.rb b/spec/consyncful/configuration_spec.rb index 88906db..b54e17b 100644 --- a/spec/consyncful/configuration_spec.rb +++ b/spec/consyncful/configuration_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Consyncful::Configuration do let(:configuration) { Consyncful::Configuration.new } - it "defaults to poll sync_mode" do + it 'defaults to poll sync_mode' do expect(configuration.sync_mode).to eq(:poll) end @@ -96,29 +96,29 @@ expect(configuration.use_webhooks?).to eq false end - it "is false if mode=webhook but credentials are missing" do + it 'is false if mode=webhook but credentials are missing' do configuration.sync_mode = :webhook configuration.webhook_user = nil configuration.webhook_password = nil expect(configuration.use_webhooks?).to eq false end - it "is true when mode=webhook and credentials are set on config" do + it 'is true when mode=webhook and credentials are set on config' do configuration.sync_mode = :webhook - configuration.webhook_user = "username" - configuration.webhook_password = "password" + configuration.webhook_user = 'username' + configuration.webhook_password = 'password' expect(configuration.use_webhooks?).to eq true end - context "when credentials are set on ENV" do + context 'when credentials are set on ENV' do before { allow(ENV).to receive(:[]).with('CONTENTFUL_WEBHOOK_USER').and_return('username') } before { allow(ENV).to receive(:[]).with('CONTENTFUL_WEBHOOK_PASSWORD').and_return('password') } - it "resolves credentials from ENV" do + it 'resolves credentials from ENV' do configuration.sync_mode = :webhook expect(configuration.use_webhooks?).to eq true - expect(configuration.resolved_webhook_user).to eq("username") - expect(configuration.resolved_webhook_password).to eq("password") + expect(configuration.resolved_webhook_user).to eq('username') + expect(configuration.resolved_webhook_password).to eq('password') end end end diff --git a/spec/controllers/consyncful/webhook_controller.rb b/spec/controllers/consyncful/webhook_controller.rb new file mode 100644 index 0000000..15113eb --- /dev/null +++ b/spec/controllers/consyncful/webhook_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.describe Consyncful::WebhookController, type: :controller do + routes { Consyncful::Engine.routes } + + let(:config) { Consyncful.configuration } + + context 'when webhooks are disabled' do + it 'returns 404 and does not signal' do + allow(config).to receive(:use_webhooks?).and_return(false) + expect(Consyncful::Sync).not_to receive(:signal_webhook!) + + post :create # POST /webhook + expect(response).to have_http_status(:not_found) + end + end + + context 'when webhooks are enabled' do + before do + Consyncful.configure do |c| + c.sync_mode = :webhook + c.webhook_user = 'user' + c.webhook_password = 'pass' + end + allow(Consyncful.configuration).to receive(:use_webhooks?).and_return(true) + end + + it 'requires HTTP Basic auth' do + post :create + expect(response).to have_http_status(:unauthorized) + expect(response.headers['WWW-Authenticate']).to include('Basic realm="Consyncful: Authenticate to Trigger Sync"') + end + + it 'rejects wrong credentials' do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('bad', 'creds') + post :create + expect(response).to have_http_status(:unauthorized) + end + + it 'accepts correct credentials and signals webhook' do + allow(Consyncful::Sync).to receive(:signal_webhook!).and_return(true) + + request.env['HTTP_AUTHORIZATION'] = + ActionController::HttpAuthentication::Basic.encode_credentials('user', 'pass') + + post :create + expect(response).to have_http_status(:accepted) + expect(Consyncful::Sync).to have_received(:signal_webhook!) + end + end +end From 0b4005af8804c961796660cf73291441cfc0c279 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 14:00:36 +1200 Subject: [PATCH 15/37] fix: Remove accidentally committed file --- .../consyncful/webhook_controller.rb | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 spec/controllers/consyncful/webhook_controller.rb diff --git a/spec/controllers/consyncful/webhook_controller.rb b/spec/controllers/consyncful/webhook_controller.rb deleted file mode 100644 index 15113eb..0000000 --- a/spec/controllers/consyncful/webhook_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Consyncful::WebhookController, type: :controller do - routes { Consyncful::Engine.routes } - - let(:config) { Consyncful.configuration } - - context 'when webhooks are disabled' do - it 'returns 404 and does not signal' do - allow(config).to receive(:use_webhooks?).and_return(false) - expect(Consyncful::Sync).not_to receive(:signal_webhook!) - - post :create # POST /webhook - expect(response).to have_http_status(:not_found) - end - end - - context 'when webhooks are enabled' do - before do - Consyncful.configure do |c| - c.sync_mode = :webhook - c.webhook_user = 'user' - c.webhook_password = 'pass' - end - allow(Consyncful.configuration).to receive(:use_webhooks?).and_return(true) - end - - it 'requires HTTP Basic auth' do - post :create - expect(response).to have_http_status(:unauthorized) - expect(response.headers['WWW-Authenticate']).to include('Basic realm="Consyncful: Authenticate to Trigger Sync"') - end - - it 'rejects wrong credentials' do - request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('bad', 'creds') - post :create - expect(response).to have_http_status(:unauthorized) - end - - it 'accepts correct credentials and signals webhook' do - allow(Consyncful::Sync).to receive(:signal_webhook!).and_return(true) - - request.env['HTTP_AUTHORIZATION'] = - ActionController::HttpAuthentication::Basic.encode_credentials('user', 'pass') - - post :create - expect(response).to have_http_status(:accepted) - expect(Consyncful::Sync).to have_received(:signal_webhook!) - end - end -end From ae15496b5b4c50a9214d45367bb427569f589063 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 14:02:55 +1200 Subject: [PATCH 16/37] refactor: use mongoid language --- lib/consyncful/sync.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/consyncful/sync.rb b/lib/consyncful/sync.rb index c4dd264..7934039 100644 --- a/lib/consyncful/sync.rb +++ b/lib/consyncful/sync.rb @@ -36,19 +36,14 @@ def self.latest ## # Signal that a webhook has been received and a sync should be triggered def self.signal_webhook! - doc = latest - # Upsert so it works even if no Sync doc exists yet; atomic set to true - collection.find({ _id: doc.id }) - .find_one_and_update({ '$set' => { 'webhook_pending' => true } }, upsert: true) + latest.set(webhook_pending: true) true end ## # Consume the webhook signal and set webhook_pending to false def self.consume_webhook_signal! - doc = latest - collection.find({ _id: doc.id, 'webhook_pending' => true }) - .find_one_and_update({ '$set' => { 'webhook_pending' => false } }) + latest.set(webhook_pending: false) end ## From b6f6368af8079cdf8eea551489f71e124e1df4f6 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 14:41:03 +1200 Subject: [PATCH 17/37] refactor: Simplify the webhook functionality --- .../consyncful/webhook_controller.rb | 14 ++-- config/routes.rb | 2 +- lib/consyncful/base.rb | 6 +- lib/consyncful/configuration.rb | 9 +-- lib/consyncful/sync_runner.rb | 69 +++---------------- lib/consyncful/tasks/consyncful.rake | 14 ++-- 6 files changed, 30 insertions(+), 84 deletions(-) diff --git a/app/controllers/consyncful/webhook_controller.rb b/app/controllers/consyncful/webhook_controller.rb index 0dae2cb..bb524d9 100644 --- a/app/controllers/consyncful/webhook_controller.rb +++ b/app/controllers/consyncful/webhook_controller.rb @@ -3,10 +3,10 @@ module Consyncful class WebhookController < ActionController::API include ActionController::HttpAuthentication::Basic::ControllerMethods - before_action :authenticate, if: -> { Consyncful.configuration.use_webhooks? } + before_action :authenticate, if: -> { Consyncful.configuration.webhook_authentication_enabled && use_webhooks? } - def create - return head :not_found unless Consyncful.configuration.use_webhooks? + def trigger_sync + return head :not_found unless use_webhooks? Consyncful::Sync.signal_webhook! head :accepted @@ -14,10 +14,14 @@ def create private + def use_webhooks? + Consyncful.configuration.sync_mode == :webhook + end + def authenticate config = Consyncful.configuration - authenticate_or_request_with_http_basic('Consyncful: Authenticate to Trigger Sync') do |user, pass| - secure_compare(user, config.resolved_webhook_user) && secure_compare(pass, config.resolved_webhook_password) + authenticate_or_request_with_http_basic('Consyncful: Authenticate to Trigger Sync') do |username, password| + secure_compare(username, config.webhook_user) && secure_compare(password, config.webhook_password) end end diff --git a/config/routes.rb b/config/routes.rb index d023781..b8d632e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true Consyncful::Engine.routes.draw do - post '/webhook', to: 'webhook#create' + post '/webhook', to: 'webhook#trigger_sync' end diff --git a/lib/consyncful/base.rb b/lib/consyncful/base.rb index 26839b1..c60555b 100644 --- a/lib/consyncful/base.rb +++ b/lib/consyncful/base.rb @@ -14,11 +14,9 @@ class Base client: Consyncful.configuration.mongo_client.to_s def self.contentful_model_name(name) - model_map = self.model_map - model_map ||= {} + self.model_map ||= {} - model_map[name] = self - model_map + self.model_map[name] = self end # rubocop:disable Lint/NestedMethodDefinition diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index f5855e6..445035a 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -12,6 +12,7 @@ class Configuration :ignore_content_tags, :preserve_contentful_timestamps, :sync_mode, + :webhook_authentication_enabled, :webhook_user, :webhook_password @@ -26,6 +27,7 @@ def initialize @ignore_content_tags = [] @preserve_contentful_timestamps = false + @webhook_authentication_enabled = true @webhook_user = nil @webhook_password = nil end @@ -40,13 +42,6 @@ def client_options options = @contentful_client_options options.reverse_merge!(DEFAULT_CLIENT_OPTIONS) end - - def resolved_webhook_user = @webhook_user || ENV['CONTENTFUL_WEBHOOK_USER'] - def resolved_webhook_password = @webhook_password || ENV['CONTENTFUL_WEBHOOK_PASSWORD'] - - def use_webhooks? - sync_mode == :webhook && resolved_webhook_user.present? && resolved_webhook_password.present? - end end DEFAULT_CLIENT_OPTIONS = { diff --git a/lib/consyncful/sync_runner.rb b/lib/consyncful/sync_runner.rb index 2ce7d2d..a473388 100644 --- a/lib/consyncful/sync_runner.rb +++ b/lib/consyncful/sync_runner.rb @@ -6,81 +6,30 @@ class SyncRunner DEFAULT_INTERVAL = 15 VALID_MODES = %i[poll webhook].freeze - def initialize(seconds: nil, mode: nil, logger: nil) + def initialize(seconds: nil, mode: nil) @interval = seconds || DEFAULT_INTERVAL - @mode = normalize_mode(mode || resolved_mode_from_config_or_env) - @logger = logger || Logger.new($stdout).tap { |l| l.progname = 'consyncful' } - @sync = Consyncful::Sync.latest - @stop = false - @shutdown_reason = nil + @mode = validate_mode(mode) end def run - trap_signals! - log "mode=#{@mode.inspect} interval=#{@interval}s" + current_sync = Consyncful::Sync.latest + current_sync.run # Run initial sync - case @mode - when :poll then run_poll - when :webhook then run_webhook - end - ensure - log "Graceful shutdown (#{@shutdown_reason}) PID=#{Process.pid}", level: :warn if @shutdown_reason - end - - private - - def run_poll loop do - break if @stop - - @sync.run sleep(@interval) - end - end - - def run_webhook - @sync.run - loop do - break if @stop - - if Consyncful::Sync.consume_webhook_signal! - @sync.run - else - sleep(@interval) + if @mode == :poll || Consyncful::Sync.consume_webhook_signal! + current_sync.run end end end - def normalize_mode(value) + private + + def validate_mode(value) sym = value.to_sym return sym if VALID_MODES.include?(sym) raise ArgumentError, "Unknown sync mode: #{sym.inspect} (expected :poll or :webhook)" end - - def resolved_mode_from_config_or_env - if Consyncful.respond_to?(:configuration) - Consyncful.configuration&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll - else - ENV['CONSYNCFUL_SYNC_MODE'] || :poll - end - end - - def trap_signals! - %w[TERM INT].each do |sig| - Signal.trap(sig) do - @shutdown_reason ||= sig - @stop = true - end - end - end - - def log(msg, level: :info) - case level - when :warn then @logger.warn(msg) - when :error then @logger.error(msg) - else @logger.info(msg) - end - end end end diff --git a/lib/consyncful/tasks/consyncful.rake b/lib/consyncful/tasks/consyncful.rake index 0a692c3..72f714c 100644 --- a/lib/consyncful/tasks/consyncful.rake +++ b/lib/consyncful/tasks/consyncful.rake @@ -11,17 +11,17 @@ namespace :consyncful do Consyncful::Sync.fresh.run end - desc "Continuously sync Contentful data. Default: poll every N seconds (default: 15).\n" \ - 'Usage: rake consyncful:sync[SECONDS] | Modes: poll (default) or webhook' + desc 'Continuously sync Contentful data. Default: poll every N seconds (default: 15)' task :sync, [:seconds] => %i[environment update_model_names] do |_task, args| require 'consyncful/sync_runner' + Signal.trap('TERM') do + puts Rainbow("Graceful shutdown PID=#{Process.pid}").red + exit 0 + end seconds = args[:seconds] - mode = if Consyncful.respond_to?(:configuration) - Consyncful.configuration&.sync_mode || ENV['CONSYNCFUL_SYNC_MODE'] || :poll - else - ENV['CONSYNCFUL_SYNC_MODE'] || :poll - end + mode = Consyncful.configuration&.sync_mode || :poll + puts "mode=#{mode.inspect} interval=#{seconds.inspect}s" Consyncful::SyncRunner.new(seconds: seconds, mode: mode).run end From 58f83975f6a0a88b38b6bd1a432cb20e466db521 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 14:52:29 +1200 Subject: [PATCH 18/37] feat: Try adding process diagram to ReadMe --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b205b6..280db0b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Consyncful -Contentful -> local database synchronisation for Rails +Contentful -> MongoDB synchronisation for Rails. + +## Why Consyncful? Requesting complicated models from the Contentful Delivery API in Rails applications is often too slow, and makes testing applications painful. Consyncful uses Contentful's synchronisation API to keep a local, up-to-date copy of the entire content in a Mongo database. @@ -8,6 +10,15 @@ Once the content is available locally, finding and interact with contentful data This gem doesn't provide any integration with the management API, or any way to update Contentful models from the local store. It is strictly read only. +```mermaid +flowchart TD + CF[Contentful (Delivery API / Sync API)] -->|sync| SY[`rake consyncful:sync`] + SY -->|writes| DB[MongoDB (single collection)] + APP[Rails models (Mongoid,
subclass Consyncful::Base)] -->|query| DB +``` + + +## Contents - [Installation](#installation) - [Usage](#usage) - [Creating contentful models in your Rails app](#creating-contentful-models-in-your-rails-app) From ce7991cd939bf62ed542dacb41d94ad6df4743f6 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 14:55:39 +1200 Subject: [PATCH 19/37] feat: Try adding process diagram to ReadMe --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 280db0b..7245781 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Consyncful -Contentful -> MongoDB synchronisation for Rails. +[Contentful](https://www.contentful.com/) -> MongoDB synchronisation for Rails. ## Why Consyncful? @@ -12,9 +12,14 @@ This gem doesn't provide any integration with the management API, or any way to ```mermaid flowchart TD - CF[Contentful (Delivery API / Sync API)] -->|sync| SY[`rake consyncful:sync`] - SY -->|writes| DB[MongoDB (single collection)] - APP[Rails models (Mongoid,
subclass Consyncful::Base)] -->|query| DB + CF["Contentful (Delivery API / Sync API)"] + SY["rake consyncful:sync"] + DB["MongoDB (single collection)"] + APP["Rails models (Mongoid, subclass Consyncful::Base)"] + + CF -->|sync| SY + SY -->|writes| DB + APP -->|queries| DB ``` From 208a8569974e2bf6deed54e19aba08cda5a097c0 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 15:15:39 +1200 Subject: [PATCH 20/37] feat: Update usage instructions --- README.md | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7245781..611ee17 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ flowchart TD APP -->|queries| DB ``` - ## Contents - [Installation](#installation) - [Usage](#usage) @@ -56,16 +55,18 @@ If you don't already use Mongoid, generate a mongoid.yml by running: $ rake g mongoid:config -Add an initializer: -Consyncful uses [contentful.rb](https://github.com/contentful/contentful.rb); client options are as documented there. Sync options are documented in the [Content Delivery Sync API docs](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization). +## Configuration + +Create config/initializers/consyncful.rb. Common options: + ```rb Consyncful.configure do |config| config.locale = 'en-NZ' config.contentful_client_options = { api_url: 'cdn.contentful.com', space: 'space_id', - access_token: 'ACCESS TOKEN', + access_token: 'ACCESS_TOKEN', environment: 'master', # optional logger: Logger.new(STDOUT) # optional for debugging } @@ -77,6 +78,24 @@ Consyncful.configure do |config| end ``` +> [!IMPORTANT] +> Consyncful uses the official [contentful.rb](https://github.com/contentful/contentful.rb) client. Any `contentful_client_options` you set are passed through to that library unchanged. Sync settings in `contentful_sync_options` map to the parameters in Contentful’s [Content Delivery Sync API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization). + + +### Option references + +| Option | Description | Default | +| --- | --- | --- | +| `locale` | Default locale when mapping fields. | `'en-NZ'` | +| `contentful_client_options` | Passed to `contentful.rb` client. | – | +| `contentful_sync_options` | Sync API params (e.g., `limit`). | Contentful defaults | +| `preserve_contentful_timestamps` | Adds `contentful_created_at` & `contentful_updated_at` to models. | `false` | +| `content_tags` | Only store entries that have **any** of these tags. | `[]` | +| `ignore_content_tags` | Ignore entries with **any** of these tags. | `[]` | +| `mongo_client` | Mongoid client to use (from `mongoid.yml`). | `:default` | +| `mongo_collection` | Collection name for all entries. | `'contentful_models'` | + + ## Usage ### Creating contentful models in your Rails app From b5c8c954969c4f02a0e3cfeac5f63b6feb56691b Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 15:26:41 +1200 Subject: [PATCH 21/37] feat: Update usage instructions --- README.md | 17 ++++++++++------- .../consyncful/webhook_controller.rb | 2 +- lib/consyncful/configuration.rb | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 611ee17..fc96d95 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ If you don't already use Mongoid, generate a mongoid.yml by running: ## Configuration -Create config/initializers/consyncful.rb. Common options: +Create `config/initializers/consyncful.rb`. An example with common configuration is: ```rb Consyncful.configure do |config| @@ -82,19 +82,22 @@ end > Consyncful uses the official [contentful.rb](https://github.com/contentful/contentful.rb) client. Any `contentful_client_options` you set are passed through to that library unchanged. Sync settings in `contentful_sync_options` map to the parameters in Contentful’s [Content Delivery Sync API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization). -### Option references +### Configuration options | Option | Description | Default | | --- | --- | --- | +| `sync_mode` | How syncing is triggered: `:poll` (periodic polling used by default) or `:webhook` (sync runs when a webhook is received from contentful). | `:poll` | +| `contentful_client_options` | Options passed through to the `contentful.rb` client. Defaults include `reuse_entries: true`, `api_url: 'cdn.contentful.com'`. | `{}` (merged with defaults) | +| `contentful_sync_options` | Contentful Sync API parameters (e.g., `limit`, `type`). Defaults include `limit: 100`, `type: 'all'`. | `{}` (merged with defaults) | | `locale` | Default locale when mapping fields. | `'en-NZ'` | -| `contentful_client_options` | Passed to `contentful.rb` client. | – | -| `contentful_sync_options` | Sync API params (e.g., `limit`). | Contentful defaults | -| `preserve_contentful_timestamps` | Adds `contentful_created_at` & `contentful_updated_at` to models. | `false` | | `content_tags` | Only store entries that have **any** of these tags. | `[]` | | `ignore_content_tags` | Ignore entries with **any** of these tags. | `[]` | +| `preserve_contentful_timestamps` | Adds `contentful_created_at` and `contentful_updated_at` to models. | `false` | | `mongo_client` | Mongoid client to use (from `mongoid.yml`). | `:default` | -| `mongo_collection` | Collection name for all entries. | `'contentful_models'` | - +| `mongo_collection` | MongoDB collection name for all entries. | `'contentful_models'` | +| `webhook_authentication_enabled` | Require Basic Auth for the webhook endpoint (only relevant when `sync_mode: :webhook`, enabled by default). | `true` | +| `webhook_user` | Username for webhook Basic Auth (when enabled). | `nil` | +| `webhook_password` | Password for webhook Basic Auth (when enabled). | `nil` | ## Usage diff --git a/app/controllers/consyncful/webhook_controller.rb b/app/controllers/consyncful/webhook_controller.rb index bb524d9..9b84c0e 100644 --- a/app/controllers/consyncful/webhook_controller.rb +++ b/app/controllers/consyncful/webhook_controller.rb @@ -3,7 +3,7 @@ module Consyncful class WebhookController < ActionController::API include ActionController::HttpAuthentication::Basic::ControllerMethods - before_action :authenticate, if: -> { Consyncful.configuration.webhook_authentication_enabled && use_webhooks? } + before_action :authenticate, if: -> { Consyncful.configuration.webhook_authentication_required && use_webhooks? } def trigger_sync return head :not_found unless use_webhooks? diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index 445035a..8fdf265 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -12,7 +12,7 @@ class Configuration :ignore_content_tags, :preserve_contentful_timestamps, :sync_mode, - :webhook_authentication_enabled, + :webhook_authentication_required, :webhook_user, :webhook_password @@ -27,7 +27,7 @@ def initialize @ignore_content_tags = [] @preserve_contentful_timestamps = false - @webhook_authentication_enabled = true + @webhook_authentication_required = true @webhook_user = nil @webhook_password = nil end From ed37b25ef9ffdfd0afab5465bd8ad0c7dfc3fb15 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 16:07:10 +1200 Subject: [PATCH 22/37] feat: Add new sync information to ReadMe --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index fc96d95..d60dafa 100644 --- a/README.md +++ b/README.md @@ -133,25 +133,78 @@ class ModelWithReferences < Consyncful::Base end ``` -### Synchronizing contentful data +## Synchronizing contentful data -To run a synchronization process run: +`Consyncful` supports **two sync modes**: +- **Polling (default)** — checks Contentful on an interval and syncs changes. +- **Webhook** — Contentful calls your app; the worker syncs when a webhook arrives. + +### Continuous sync (either mode) +Run the same task in both modes — the behaviour depends on your configuration: $ rake consyncful:sync -The first time you run this it will download all the Contentful content. It will then check every 15 seconds for changes to the content and update/delete records in the database when changes are made in Contentful. +- **Polling mode**: after the initial full sync, the worker polls every **15s** (configurable) and applies changes it finds. +- **Webhook mode**: after the initial full sync, the worker **does not poll**. It waits for a webhook signal and then runs a sync. + + +> [!NOTE] +> The first time you run this it will download all the Contentful content. -If you want to synchronise from scratch, run: +### Refresh from scratch + +If you want to resynchronize everything (e.g., after model/content type renames), run: $ rake consyncful:refresh +This performs a full rebuild of data from contentful. + It is recommended to refresh your data if you change model names. Now you've synced your data, it is all available via your Rails models. -### Finding and interacting with models +### Enabling webhook mode + +#### 1. Set the sync mode to webhook + +``` +# e.g. config/initializers/consyncful.rb +Consyncful.configure do |c| + c.sync_mode = :webhook +end +``` + +#### 2. Mount the webhooks controller: +Expose the engine so Contentful can POST to it +``` +# config/routes.rb +mount Consyncful::Engine, at: "/consyncful" +``` +The webhook endpoint lives under this mount (e.g. `/consyncful/trigger_sync`). + +#### 3. Authentication (recommended) +Webhook authentication is **on by default**: +``` +Consyncful.configure do |c| + c.webhook_authentication_required = true # default + c.webhook_user = ENV["CONSYNCFUL_WEBHOOK_USER"] + c.webhook_password = ENV["CONSYNCFUL_WEBHOOK_PASSWORD"] +end +``` +To accept webhooks **without** auth (not recommended), explicitly disable it: +``` +c.webhook_authentication_required = false +``` + +#### 4. Create the webhook in Contentful +In your Contentful space/environment, add a webhook that points to your mounted route (e.g. `https://your-app.example.com/consyncful/trigger_sync`) and select which events should trigger a sync (publish/unpublish, entries, assets, etc.). See Contentful documents here for information on setting up a webhook: [Configuring a webhook](https://www.contentful.com/developers/docs/webhooks/configure-webhook/) + +> [!IMPORTANT] +> If your application is behind global authentication, VPN, or an allowlist, Contentful won’t be able to reach the webhook endpoint. Ensure that `POST` requests from Contentful can reach your mounted path (e.g. `/consyncful/...`). In many setups this means adding an ingress rule or route exemption for the webhook path. Keeping webhook authentication **enabled** (default) is recommended; configure matching credentials in the Contentful webhook. + +## Finding and interacting with models -#### Querying +### Querying Models are available using standard Mongoid [queries](https://docs.mongodb.com/mongoid/current/tutorials/mongoid-queries/). ```ruby @@ -160,7 +213,7 @@ instance = ModelName.find_by(instance: 'foo') instance.is_awesome # true ``` -#### References +### References References work like you would expect: ```ruby @@ -178,7 +231,7 @@ instance.other_things # all the referenced things, polymorphic, so might be diff instance.other_things.in_order # ordered the same as in Contentful ``` -#### Finding entries from different content types +### Finding entries from different content types Because all Contentful models are stored as polymorphic subtypes of `Consyncful::Base`, you can query all entries without knowing what type you are looking for: @@ -186,7 +239,7 @@ Because all Contentful models are stored as polymorphic subtypes of `Consyncful: Consyncful::Base.where(title: 'a title') # [ #, # ] ``` -### Sync callbacks +## Sync callbacks You may want to attach some application logic to happen before or after a sync run, for example to update caches. @@ -204,7 +257,7 @@ Consyncful::Sync.after_run do |updated_ids| end ``` -### Using Locales for specific fields +## Using Locales for specific fields If fields have multiple locales then the default locale will be mapped to the field name. Additional locales will have a suffix (lower snake case) on the field name. e.g title (default), title_mi_nz (New Zealand Maori mi-NZ) @@ -221,7 +274,7 @@ Consyncful.configure do |config| end ``` -### Sync specific contents using [Contentful Tag](https://www.contentful.com/help/tags/). +## Sync specific contents using [Contentful Tag](https://www.contentful.com/help/tags/). You can configure Consyncful to sync or ignore specific contents using Contentful Tag. ```rb @@ -241,7 +294,7 @@ Consyncful.configure do |config| end ``` -### Configuring what Mongo database Consyncful uses +## Configuring what Mongo database Consyncful uses You can also configure what Mongoid client Consyncful uses and the name of the collection the entries are stored under. This is useful if you want to have your consyncful data hosted in a different mongo database than your application-specific mongo database. @@ -252,7 +305,7 @@ Consyncful.configure do |config| end ``` -### Why do I have to use MongoDB? +## Why do I have to use MongoDB? Consyncful currently only supports Mongoid ODM because models have dynamic schemas. And that's all we've had a chance to work out so far. The same pattern might be able to be extended to work with ActiveRecord, but having to migrate the local database as well as your contentful content type's seems tedious. From 3ddf3a6625fc62caf851459bff6578c80a043717 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 16:28:32 +1200 Subject: [PATCH 23/37] fix: Readme tidy up --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d60dafa..185e12c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ flowchart TD ``` ## Contents -- [Installation](#installation) +- [Setup](#setup) + - [Installation](#installation) + - [Configuration](#configuration) - [Usage](#usage) - [Creating contentful models in your Rails app](#creating-contentful-models-in-your-rails-app) - [Synchronizing contentful data](#synchronizing-contentful-data) @@ -39,7 +41,9 @@ flowchart TD - [Contributing](#contributing) - [License](#license) -## Installation +## Setup + +### Installation Add this line to your application's Gemfile: @@ -56,7 +60,7 @@ If you don't already use Mongoid, generate a mongoid.yml by running: $ rake g mongoid:config -## Configuration +### Configuration Create `config/initializers/consyncful.rb`. An example with common configuration is: @@ -82,7 +86,7 @@ end > Consyncful uses the official [contentful.rb](https://github.com/contentful/contentful.rb) client. Any `contentful_client_options` you set are passed through to that library unchanged. Sync settings in `contentful_sync_options` map to the parameters in Contentful’s [Content Delivery Sync API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization). -### Configuration options +#### Configuration options | Option | Description | Default | | --- | --- | --- | @@ -99,9 +103,7 @@ end | `webhook_user` | Username for webhook Basic Auth (when enabled). | `nil` | | `webhook_password` | Password for webhook Basic Auth (when enabled). | `nil` | -## Usage - -### Creating contentful models in your Rails app +## Creating contentful models in your Rails application Create models by inheriting from `Consyncful::Base` From eeb113b70aad78f911edfc584bea8b53fb6faf8b Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 16:35:27 +1200 Subject: [PATCH 24/37] feat: Add note about rate limits --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 185e12c..35af2c9 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,13 @@ Now you've synced your data, it is all available via your Rails models. ### Enabling webhook mode +> [!TIP] **Prefer Webhook mode on limited plans** +> Polling makes API requests on every interval (default ~15s), which can quickly add up and exhaust quotas on lower-tier Contentful plans. +> **Webhook mode** only syncs when Contentful sends an event, dramatically reducing API calls. If you’re hitting rate limits—or want to avoid them—switch to `:webhook`. +> +> If you are hitting API rate limits and need to use polling, consider increasing the interval to reduce load. + + #### 1. Set the sync mode to webhook ``` From be12f631a811dcea5aad9a96943ba1acc281e56a Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Fri, 15 Aug 2025 16:40:44 +1200 Subject: [PATCH 25/37] feat: Add note about rate limits --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 35af2c9..6c7d3b1 100644 --- a/README.md +++ b/README.md @@ -167,10 +167,10 @@ Now you've synced your data, it is all available via your Rails models. ### Enabling webhook mode -> [!TIP] **Prefer Webhook mode on limited plans** +> [!TIP] +> **Prefer Webhook mode on limited plans** > Polling makes API requests on every interval (default ~15s), which can quickly add up and exhaust quotas on lower-tier Contentful plans. > **Webhook mode** only syncs when Contentful sends an event, dramatically reducing API calls. If you’re hitting rate limits—or want to avoid them—switch to `:webhook`. -> > If you are hitting API rate limits and need to use polling, consider increasing the interval to reduce load. From b248f12b1ebaf06821400a8e9652b59bafddd821 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 08:48:39 +1200 Subject: [PATCH 26/37] chore: Make tips more usable --- README.md | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6c7d3b1..75d6753 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [Contentful](https://www.contentful.com/) -> MongoDB synchronisation for Rails. -## Why Consyncful? - Requesting complicated models from the Contentful Delivery API in Rails applications is often too slow, and makes testing applications painful. Consyncful uses Contentful's synchronisation API to keep a local, up-to-date copy of the entire content in a Mongo database. Once the content is available locally, finding and interact with contentful data is as easy as using [Mongoid](https://docs.mongodb.com/mongoid/current/tutorials/mongoid-documents/) ODM. @@ -24,15 +22,13 @@ flowchart TD ## Contents - [Setup](#setup) - - [Installation](#installation) - - [Configuration](#configuration) -- [Usage](#usage) - - [Creating contentful models in your Rails app](#creating-contentful-models-in-your-rails-app) - - [Synchronizing contentful data](#synchronizing-contentful-data) - - [Finding and interacting with models](#finding-and-interacting-with-models) - - [Querying](#querying) - - [References](#references) - - [Finding entries from different content types](#finding-entries-from-different-content-types) +- [Creating contentful models in your Rails app](#creating-contentful-models-in-your-rails-app) +- [Synchronizing contentful data](#synchronizing-contentful-data) +- [Finding and interacting with models](#finding-and-interacting-with-models) + - [Querying](#querying) + - [References](#references) + - [Finding entries from different content types](#finding-entries-from-different-content-types) + - [Sync callbacks](#sync-callbacks) - [Using Locales for specific fields](#using-locales-for-specific-fields) - [Configuring what Mongo database Consyncful uses](#configuring-what-mongo-database-consyncful-uses) @@ -59,9 +55,6 @@ If you don't already use Mongoid, generate a mongoid.yml by running: $ rake g mongoid:config - -### Configuration - Create `config/initializers/consyncful.rb`. An example with common configuration is: ```rb @@ -82,8 +75,8 @@ Consyncful.configure do |config| end ``` -> [!IMPORTANT] -> Consyncful uses the official [contentful.rb](https://github.com/contentful/contentful.rb) client. Any `contentful_client_options` you set are passed through to that library unchanged. Sync settings in `contentful_sync_options` map to the parameters in Contentful’s [Content Delivery Sync API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization). +> [!NOTE] +> Consyncful uses the official [contentful.rb](https://github.com/contentful/contentful.rb) client. Any `contentful_client_options` you set are passed through to this library unchanged. Similary, settings in `contentful_sync_options` map to the parameters in Contentful’s [Content Delivery Sync API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization). #### Configuration options @@ -168,7 +161,7 @@ Now you've synced your data, it is all available via your Rails models. ### Enabling webhook mode > [!TIP] -> **Prefer Webhook mode on limited plans** +> **Webhook mode is recommended on limited plans**. > Polling makes API requests on every interval (default ~15s), which can quickly add up and exhaust quotas on lower-tier Contentful plans. > **Webhook mode** only syncs when Contentful sends an event, dramatically reducing API calls. If you’re hitting rate limits—or want to avoid them—switch to `:webhook`. > If you are hitting API rate limits and need to use polling, consider increasing the interval to reduce load. From dae61f1e670817be1f0b81da1135b5596caad89d Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 09:13:12 +1200 Subject: [PATCH 27/37] chore: Update ToC --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 75d6753..9b16c93 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,17 @@ flowchart TD - [Setup](#setup) - [Creating contentful models in your Rails app](#creating-contentful-models-in-your-rails-app) - [Synchronizing contentful data](#synchronizing-contentful-data) + - [Continuous sync](#continuous-sync-either-mode) + - [Refresh from scratch](#refresh-from-scratch) + - [Enabling webhook mode](#enabling-webhook-mode) - [Finding and interacting with models](#finding-and-interacting-with-models) - [Querying](#querying) - [References](#references) - [Finding entries from different content types](#finding-entries-from-different-content-types) - - - [Sync callbacks](#sync-callbacks) - - [Using Locales for specific fields](#using-locales-for-specific-fields) - - [Configuring what Mongo database Consyncful uses](#configuring-what-mongo-database-consyncful-uses) - - [Why do I have to use MongoDB?](#why-do-i-have-to-use-mongodb) +- [Sync callbacks](#sync-callbacks) +- [Using Locales for specific fields](#using-locales-for-specific-fields) +- [Configuring what Mongo database Consyncful uses](#configuring-what-mongo-database-consyncful-uses) +- [Why do I have to use MongoDB?](#why-do-i-have-to-use-mongodb) - [Development](#development) - [Contributing](#contributing) - [License](#license) From 69feb2af3bd94cc01501112459dff0a4db158874 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 10:47:52 +1200 Subject: [PATCH 28/37] chore: reorder readme contents --- README.md | 79 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 9b16c93..ff75ac0 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,12 @@ flowchart TD - [Continuous sync](#continuous-sync-either-mode) - [Refresh from scratch](#refresh-from-scratch) - [Enabling webhook mode](#enabling-webhook-mode) + - [Sync callbacks](#sync-callbacks) + - [Sync specific contents using contentful tags](#sync-specific-contents-using-contentful-tag) - [Finding and interacting with models](#finding-and-interacting-with-models) - [Querying](#querying) - [References](#references) - [Finding entries from different content types](#finding-entries-from-different-content-types) -- [Sync callbacks](#sync-callbacks) - [Using Locales for specific fields](#using-locales-for-specific-fields) - [Configuring what Mongo database Consyncful uses](#configuring-what-mongo-database-consyncful-uses) - [Why do I have to use MongoDB?](#why-do-i-have-to-use-mongodb) @@ -206,6 +207,44 @@ In your Contentful space/environment, add a webhook that points to your mounted > [!IMPORTANT] > If your application is behind global authentication, VPN, or an allowlist, Contentful won’t be able to reach the webhook endpoint. Ensure that `POST` requests from Contentful can reach your mounted path (e.g. `/consyncful/...`). In many setups this means adding an ingress rule or route exemption for the webhook path. Keeping webhook authentication **enabled** (default) is recommended; configure matching credentials in the Contentful webhook. +### Sync callbacks + +You may want to attach some application logic to happen before or after a sync run, for example to update caches. + +Callbacks can be registered using: + +```ruby +Consyncful::Sync.before_run do + # do something before the run +end +``` + +```ruby +Consyncful::Sync.after_run do |updated_ids| + # invalidate cache for updated_ids, or something +end +``` + +### Sync specific contents using [Contentful Tag](https://www.contentful.com/help/tags/) +You can configure Consyncful to sync or ignore specific contents using Contentful Tag. + +```rb +Consyncful.configure do |config| + # Any contents tagged with 'myTag' will be stored in the database. + # Other contents without 'myTag' would be ignored. + config.content_tags = ['myTag'] # defaults to [] +end +``` + +Also, you can ignore contents with specific Tags. + +```rb +Consyncful.configure do |config| + # Any contents tagged with 'ignoreTag' won't be stored in the database. + config.ignore_content_tags = ['ignoreTag'] # defaults to [] +end +``` + ## Finding and interacting with models ### Querying @@ -243,24 +282,6 @@ Because all Contentful models are stored as polymorphic subtypes of `Consyncful: Consyncful::Base.where(title: 'a title') # [ #, # ] ``` -## Sync callbacks - -You may want to attach some application logic to happen before or after a sync run, for example to update caches. - -Callbacks can be registered using: - -```ruby -Consyncful::Sync.before_run do - # do something before the run -end -``` - -```ruby -Consyncful::Sync.after_run do |updated_ids| - # invalidate cache for updated_ids, or something -end -``` - ## Using Locales for specific fields If fields have multiple locales then the default locale will be mapped to the field name. Additional locales will have a suffix (lower snake case) on the field name. e.g title (default), title_mi_nz (New Zealand Maori mi-NZ) @@ -278,26 +299,6 @@ Consyncful.configure do |config| end ``` -## Sync specific contents using [Contentful Tag](https://www.contentful.com/help/tags/). -You can configure Consyncful to sync or ignore specific contents using Contentful Tag. - -```rb -Consyncful.configure do |config| - # Any contents tagged with 'myTag' will be stored in the database. - # Other contents without 'myTag' would be ignored. - config.content_tags = ['myTag'] # defaults to [] -end -``` - -Also, you can ignore contents with specific Tags. - -```rb -Consyncful.configure do |config| - # Any contents tagged with 'ignoreTag' won't be stored in the database. - config.ignore_content_tags = ['ignoreTag'] # defaults to [] -end -``` - ## Configuring what Mongo database Consyncful uses You can also configure what Mongoid client Consyncful uses and the name of the collection the entries are stored under. This is useful if you want to have your consyncful data hosted in a different mongo database than your application-specific mongo database. From c183c5b0e3a15b465f1d723c34d3c52e074afce9 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 11:12:14 +1200 Subject: [PATCH 29/37] chore: Reorder readme content to logical groups --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ff75ac0..ab36f09 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ flowchart TD ## Contents - [Setup](#setup) + - [Installation](#installation) + - [Configuration options](#configuration-options) - [Creating contentful models in your Rails app](#creating-contentful-models-in-your-rails-app) - [Synchronizing contentful data](#synchronizing-contentful-data) - [Continuous sync](#continuous-sync-either-mode) @@ -33,9 +35,10 @@ flowchart TD - [Querying](#querying) - [References](#references) - [Finding entries from different content types](#finding-entries-from-different-content-types) -- [Using Locales for specific fields](#using-locales-for-specific-fields) -- [Configuring what Mongo database Consyncful uses](#configuring-what-mongo-database-consyncful-uses) -- [Why do I have to use MongoDB?](#why-do-i-have-to-use-mongodb) + - [Using Locales for specific fields](#using-locales-for-specific-fields) +- [MongoDB Configuration](#mongodb-configuration) + - [Choosing the Mongo Database](#choosing-the-mongo-database) + - [Why MongoDB?](#why-mongodb) - [Development](#development) - [Contributing](#contributing) - [License](#license) @@ -82,7 +85,7 @@ end > Consyncful uses the official [contentful.rb](https://github.com/contentful/contentful.rb) client. Any `contentful_client_options` you set are passed through to this library unchanged. Similary, settings in `contentful_sync_options` map to the parameters in Contentful’s [Content Delivery Sync API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization). -#### Configuration options +### Configuration options | Option | Description | Default | | --- | --- | --- | @@ -282,11 +285,11 @@ Because all Contentful models are stored as polymorphic subtypes of `Consyncful: Consyncful::Base.where(title: 'a title') # [ #, # ] ``` -## Using Locales for specific fields +### Using Locales for specific fields If fields have multiple locales then the default locale will be mapped to the field name. Additional locales will have a suffix (lower snake case) on the field name. e.g title (default), title_mi_nz (New Zealand Maori mi-NZ) -### Preserving Contentful timestamps +#### Preserving Contentful timestamps If you need to access the timestamps from Contentful, you can enable it by setting `preserve_contentful_timestamps` to `true`. @@ -299,9 +302,11 @@ Consyncful.configure do |config| end ``` -## Configuring what Mongo database Consyncful uses +## MongoDB Configuration -You can also configure what Mongoid client Consyncful uses and the name of the collection the entries are stored under. This is useful if you want to have your consyncful data hosted in a different mongo database than your application-specific mongo database. +### Choosing the Mongo Database + +You can configure which Mongoid client Consyncful uses, as well as the name of the collection where entries are stored. This is useful if you want Consyncful data to live in a separate MongoDB database from your application-specific database. ```rb Consyncful.configure do |config| @@ -310,9 +315,9 @@ Consyncful.configure do |config| end ``` -## Why do I have to use MongoDB? +### Why MongoDB? -Consyncful currently only supports Mongoid ODM because models have dynamic schemas. And that's all we've had a chance to work out so far. The same pattern might be able to be extended to work with ActiveRecord, but having to migrate the local database as well as your contentful content type's seems tedious. +Consyncful currently only supports the Mongoid ODM because models require dynamic schemas. Extending support to ActiveRecord could be possible in the future, but it would also require maintaining database migrations alongside Contentful content type changes—which adds complexity we wanted to avoid. ## Development From d24ede63645a076ba151232882a129b45bbf6b9f Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 11:22:31 +1200 Subject: [PATCH 30/37] fix: Add missing heading to contents --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ab36f09..e3d4959 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ flowchart TD - [References](#references) - [Finding entries from different content types](#finding-entries-from-different-content-types) - [Using Locales for specific fields](#using-locales-for-specific-fields) + - [Preserving Contentful timestamps](#preserving-contentful-timestamps) - [MongoDB Configuration](#mongodb-configuration) - [Choosing the Mongo Database](#choosing-the-mongo-database) - [Why MongoDB?](#why-mongodb) @@ -289,7 +290,7 @@ Consyncful::Base.where(title: 'a title') # [ #, # ] If fields have multiple locales then the default locale will be mapped to the field name. Additional locales will have a suffix (lower snake case) on the field name. e.g title (default), title_mi_nz (New Zealand Maori mi-NZ) -#### Preserving Contentful timestamps +### Preserving Contentful timestamps If you need to access the timestamps from Contentful, you can enable it by setting `preserve_contentful_timestamps` to `true`. From 74e3dc33f7bfb7bdfc86d950b837c31b654ca2a7 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 12:09:26 +1200 Subject: [PATCH 31/37] tests: Add tests for the controller --- Gemfile | 7 - Gemfile.lock | 106 +++++++++++++- config/routes.rb | 2 +- consyncful.gemspec | 11 ++ lib/consyncful/engine.rb | 2 +- spec/consyncful/configuration_spec.rb | 32 ----- .../consyncful/webhook_controller_spec.rb | 89 ++++++++++++ spec/internal/config/routes.rb | 3 + spec/internal/log/test.log | 130 ++++++++++++++++++ spec/rails_helper.rb | 17 +++ 10 files changed, 356 insertions(+), 43 deletions(-) create mode 100644 spec/controllers/consyncful/webhook_controller_spec.rb create mode 100644 spec/internal/config/routes.rb create mode 100644 spec/internal/log/test.log create mode 100644 spec/rails_helper.rb diff --git a/Gemfile b/Gemfile index ffb8b22..4cb9aa5 100644 --- a/Gemfile +++ b/Gemfile @@ -3,19 +3,12 @@ source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } - -# gem 'activemodel', '~> 7' -# gem 'activesupport', '~> 7' -# gem 'mongoid', '~> 8' - # Specify your gem's dependencies in consyncful.gemspec gemspec group :development do gem 'bundler', '~> 2' - gem 'database_cleaner-mongoid' gem 'rake', '~> 13.0' - gem 'rspec', '~> 3.0' gem 'rubocop' gem 'rubocop-rake' gem 'rubocop-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index 473c612..c6caac7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,6 +10,23 @@ PATH GEM remote: https://rubygems.org/ specs: + actionpack (7.2.2.1) + actionview (= 7.2.2.1) + activesupport (= 7.2.2.1) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.2) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionview (7.2.2.1) + activesupport (= 7.2.2.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) activemodel (7.2.2.1) activesupport (= 7.2.2.1) activesupport (7.2.2.1) @@ -31,18 +48,27 @@ GEM benchmark (0.4.0) bigdecimal (3.1.9) bson (5.0.2) + builder (3.3.0) + combustion (1.5.0) + activesupport (>= 3.0.0) + railties (>= 3.0.0) + thor (>= 0.14.6) concurrent-ruby (1.3.5) connection_pool (2.5.3) contentful (2.17.1) http (> 0.8, < 6.0) multi_json (~> 1) + crass (1.0.6) database_cleaner-core (2.0.1) database_cleaner-mongoid (2.0.1) database_cleaner-core (~> 2.0.0) mongoid + date (3.4.1) diff-lcs (1.6.2) domain_name (0.6.20240107) drb (2.2.3) + erb (5.0.2) + erubi (1.13.1) ffi (1.17.2) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) @@ -70,6 +96,11 @@ GEM http-form_data (2.3.0) i18n (1.14.7) concurrent-ruby (~> 1.0) + io-console (0.8.1) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) json (2.12.2) language_server-protocol (3.17.0.5) lint_roller (1.1.0) @@ -77,6 +108,10 @@ GEM ffi-compiler (~> 1.0) rake (~> 13.0) logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mini_portile2 (2.8.9) minitest (5.25.5) mongo (2.21.1) base64 @@ -87,16 +122,69 @@ GEM mongo (>= 2.18.0, < 3.0.0) ruby2_keywords (~> 0.0.5) multi_json (1.15.0) + nokogiri (1.18.9) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.9-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-linux-musl) + racc (~> 1.4) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) prism (1.4.0) + psych (5.2.6) + date + stringio public_suffix (6.0.2) racc (1.8.1) + rack (3.1.16) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.2.2.1) + actionpack (= 7.2.2.1) + activesupport (= 7.2.2.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) + rdoc (6.14.2) + erb + psych (>= 4.0.0) regexp_parser (2.10.0) + reline (0.6.2) + io-console (~> 0.5) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -109,6 +197,14 @@ GEM rspec-mocks (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) rspec-support (3.13.3) rubocop (1.75.7) json (~> 2.3) @@ -133,12 +229,16 @@ GEM ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) securerandom (0.4.1) + stringio (3.1.7) + thor (1.4.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.0.15) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + useragent (0.16.11) + zeitwerk (2.7.3) PLATFORMS aarch64-linux-gnu @@ -155,10 +255,12 @@ PLATFORMS DEPENDENCIES bundler (~> 2) + combustion (~> 1.3) consyncful! - database_cleaner-mongoid + database_cleaner-mongoid (~> 2.0) rake (~> 13.0) - rspec (~> 3.0) + rspec (~> 3.13) + rspec-rails (~> 6.1) rubocop rubocop-rake rubocop-rspec diff --git a/config/routes.rb b/config/routes.rb index b8d632e..d54710b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true Consyncful::Engine.routes.draw do - post '/webhook', to: 'webhook#trigger_sync' + post 'webhook', to: 'webhook#trigger_sync' end diff --git a/consyncful.gemspec b/consyncful.gemspec index f6a8947..5cecb2a 100644 --- a/consyncful.gemspec +++ b/consyncful.gemspec @@ -31,9 +31,20 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] + # + # Runtime dependencies + # spec.add_dependency 'contentful', ['>=2.11.1', '<3.0.0'] spec.add_dependency 'hooks', '>=0.4.1' spec.add_dependency 'mongoid', '>=7.0.2' spec.add_dependency 'rainbow' spec.metadata['rubygems_mfa_required'] = 'true' + + # + # Development / test dependencies (installed in CI because Gemfile has `gemspec`) + # + spec.add_development_dependency "rspec", "~> 3.13" + spec.add_development_dependency "rspec-rails", "~> 6.1" + spec.add_development_dependency "combustion", "~> 1.3" + spec.add_development_dependency "database_cleaner-mongoid", "~> 2.0" end diff --git a/lib/consyncful/engine.rb b/lib/consyncful/engine.rb index 8a5d384..137bda8 100644 --- a/lib/consyncful/engine.rb +++ b/lib/consyncful/engine.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Consyncful - class Engine < Rails::Engine + class Engine < ::Rails::Engine isolate_namespace Consyncful end end diff --git a/spec/consyncful/configuration_spec.rb b/spec/consyncful/configuration_spec.rb index b54e17b..878e460 100644 --- a/spec/consyncful/configuration_spec.rb +++ b/spec/consyncful/configuration_spec.rb @@ -90,36 +90,4 @@ end end end - - describe '.use_webhooks?' do - it 'is false by default' do - expect(configuration.use_webhooks?).to eq false - end - - it 'is false if mode=webhook but credentials are missing' do - configuration.sync_mode = :webhook - configuration.webhook_user = nil - configuration.webhook_password = nil - expect(configuration.use_webhooks?).to eq false - end - - it 'is true when mode=webhook and credentials are set on config' do - configuration.sync_mode = :webhook - configuration.webhook_user = 'username' - configuration.webhook_password = 'password' - expect(configuration.use_webhooks?).to eq true - end - - context 'when credentials are set on ENV' do - before { allow(ENV).to receive(:[]).with('CONTENTFUL_WEBHOOK_USER').and_return('username') } - before { allow(ENV).to receive(:[]).with('CONTENTFUL_WEBHOOK_PASSWORD').and_return('password') } - - it 'resolves credentials from ENV' do - configuration.sync_mode = :webhook - expect(configuration.use_webhooks?).to eq true - expect(configuration.resolved_webhook_user).to eq('username') - expect(configuration.resolved_webhook_password).to eq('password') - end - end - end end diff --git a/spec/controllers/consyncful/webhook_controller_spec.rb b/spec/controllers/consyncful/webhook_controller_spec.rb new file mode 100644 index 0000000..299a7e0 --- /dev/null +++ b/spec/controllers/consyncful/webhook_controller_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe "Consyncful::WebhookController", type: :request do + let(:config) do + # Provide a simple configuration double with the fields used by the controller + instance_double( + "Consyncful::Configuration", + sync_mode: sync_mode, + webhook_authentication_required: auth_required, + webhook_user: "user1", + webhook_password: "secret" + ) + end + + before do + allow(Consyncful).to receive(:configuration).and_return(config) + end + + describe "POST /consyncful/webhook" do + subject(:perform) { post "/consyncful/webhook", headers: headers } + + let(:headers) { {} } + + context "when sync mode is not :webhook" do + let(:sync_mode) { :poll } + let(:auth_required) { false } + + it "returns 404 Not Found" do + perform + expect(response).to have_http_status(:not_found) + end + end + + context "when sync mode is :webhook" do + let(:sync_mode) { :webhook } + + context "and authentication is not required" do + let(:auth_required) { false } + + it "signals the sync and returns 202 Accepted" do + expect(Consyncful::Sync).to receive(:signal_webhook!) + perform + expect(response).to have_http_status(:accepted) + end + end + + context "and authentication is required" do + let(:auth_required) { true } + + context "with no credentials" do + it "returns 401 Unauthorized" do + perform + expect(response).to have_http_status(:unauthorized) + end + end + + context "with wrong credentials" do + let(:headers) do + { + "HTTP_AUTHORIZATION" => + ActionController::HttpAuthentication::Basic.encode_credentials("user1", "wrong") + } + end + + it "returns 401 Unauthorized" do + perform + expect(response).to have_http_status(:unauthorized) + end + end + + context "with correct credentials" do + let(:headers) do + { + "HTTP_AUTHORIZATION" => + ActionController::HttpAuthentication::Basic.encode_credentials("user1", "secret") + } + end + + it "signals the sync and returns 202 Accepted" do + expect(Consyncful::Sync).to receive(:signal_webhook!) + perform + expect(response).to have_http_status(:accepted) + end + end + end + end + end +end diff --git a/spec/internal/config/routes.rb b/spec/internal/config/routes.rb new file mode 100644 index 0000000..a067639 --- /dev/null +++ b/spec/internal/config/routes.rb @@ -0,0 +1,3 @@ +Rails.application.routes.draw do + mount Consyncful::Engine => "/consyncful" +end \ No newline at end of file diff --git a/spec/internal/log/test.log b/spec/internal/log/test.log new file mode 100644 index 0000000..9de5ba0 --- /dev/null +++ b/spec/internal/log/test.log @@ -0,0 +1,130 @@ +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:40 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:56 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:35:25 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:39:33 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:41:48 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:44:01 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:45:14 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:46:37 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:53:39 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:55:49 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:59:22 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:00:41 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:01:37 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:04:07 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:04:21 +1200 + +ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): + +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:06:32 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 10ms (GC: 6.1ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 404 Not Found in 3ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Filter chain halted as :authenticate rendered or redirected +Completed 401 Unauthorized in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Filter chain halted as :authenticate rendered or redirected +Completed 401 Unauthorized in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 10ms (GC: 4.6ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Filter chain halted as :authenticate rendered or redirected +Completed 401 Unauthorized in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Filter chain halted as :authenticate rendered or redirected +Completed 401 Unauthorized in 0ms (GC: 0.1ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 404 Not Found in 2ms (GC: 0.2ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Filter chain halted as :authenticate rendered or redirected +Completed 401 Unauthorized in 0ms (GC: 0.1ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Filter chain halted as :authenticate rendered or redirected +Completed 401 Unauthorized in 0ms (GC: 0.0ms) +Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 +Processing by Consyncful::WebhookController#trigger_sync as HTML +Completed 202 Accepted in 0ms (GC: 0.0ms) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..3b17834 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +ENV["RAILS_ENV"] ||= "test" + +require "bundler/setup" +require "combustion" +require "consyncful" +require "consyncful/engine" + +Combustion.initialize! :action_controller, :active_support + +require "rspec/rails" + +RSpec.configure do |config| + config.use_active_record = false + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! +end From 0496794038c4f74f3f8a4148b664f5e501e6b514 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 12:37:11 +1200 Subject: [PATCH 32/37] fix: Add class comments to fix issues --- Gemfile | 10 ++++ .../consyncful/webhook_controller.rb | 19 ++++++++ consyncful.gemspec | 11 ----- lib/consyncful/configuration.rb | 14 ++++++ lib/consyncful/engine.rb | 5 ++ lib/consyncful/sync_runner.rb | 16 +++++-- .../consyncful/webhook_controller_spec.rb | 47 ++++++++++--------- spec/internal/config/routes.rb | 6 ++- spec/rails_helper.rb | 13 ++--- 9 files changed, 95 insertions(+), 46 deletions(-) diff --git a/Gemfile b/Gemfile index 4cb9aa5..36e1674 100644 --- a/Gemfile +++ b/Gemfile @@ -13,3 +13,13 @@ group :development do gem 'rubocop-rake' gem 'rubocop-rspec' end + +group :development, :test do + gem 'combustion', '~> 1.3' + gem 'rspec', '~> 3.13' + gem 'rspec-rails', '~> 6.1' +end + +group :test do + gem 'database_cleaner-mongoid', '~> 2.0' +end diff --git a/app/controllers/consyncful/webhook_controller.rb b/app/controllers/consyncful/webhook_controller.rb index 9b84c0e..a38ad26 100644 --- a/app/controllers/consyncful/webhook_controller.rb +++ b/app/controllers/consyncful/webhook_controller.rb @@ -1,6 +1,25 @@ # frozen_string_literal: true module Consyncful + # The Consyncful::WebhookController is responsible for handling incoming + # webhook requests that can trigger synchronization jobs within Consyncful. + # + # Features: + # - Only responds to requests if `sync_mode` is configured as `:webhook`. + # - Optionally requires HTTP Basic authentication if + # `webhook_authentication_required` is enabled in configuration. + # - Exposes a single endpoint (`trigger_sync`) that signals a sync process + # through `Consyncful::Sync.signal_webhook!`. + # + # Security: + # - Uses `ActionController::HttpAuthentication::Basic` to enforce + # authentication when enabled. + # - Compares provided credentials with configured values using + # `ActiveSupport::SecurityUtils.secure_compare` to prevent timing attacks. + # + # Responses: + # - Returns `404 Not Found` if webhooks are not enabled. + # - Returns `202 Accepted` after signaling a sync. class WebhookController < ActionController::API include ActionController::HttpAuthentication::Basic::ControllerMethods before_action :authenticate, if: -> { Consyncful.configuration.webhook_authentication_required && use_webhooks? } diff --git a/consyncful.gemspec b/consyncful.gemspec index 5cecb2a..f6a8947 100644 --- a/consyncful.gemspec +++ b/consyncful.gemspec @@ -31,20 +31,9 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - # - # Runtime dependencies - # spec.add_dependency 'contentful', ['>=2.11.1', '<3.0.0'] spec.add_dependency 'hooks', '>=0.4.1' spec.add_dependency 'mongoid', '>=7.0.2' spec.add_dependency 'rainbow' spec.metadata['rubygems_mfa_required'] = 'true' - - # - # Development / test dependencies (installed in CI because Gemfile has `gemspec`) - # - spec.add_development_dependency "rspec", "~> 3.13" - spec.add_development_dependency "rspec-rails", "~> 6.1" - spec.add_development_dependency "combustion", "~> 1.3" - spec.add_development_dependency "database_cleaner-mongoid", "~> 2.0" end diff --git a/lib/consyncful/configuration.rb b/lib/consyncful/configuration.rb index 8fdf265..bd365c6 100644 --- a/lib/consyncful/configuration.rb +++ b/lib/consyncful/configuration.rb @@ -2,6 +2,18 @@ # Handles Rails configurations for Consyncful module Consyncful + # Provides configuration options for Consyncful, including: + # - Contentful API client and sync options + # - MongoDB client and collection settings + # - Locale and content tag filtering + # - Sync mode (poll or webhook) + # - Webhook authentication credentials + # + # This class is typically accessed and customized via + # Consyncful.configure do |config| + # config.locale = 'en-NZ' + # config.mongo_collection = 'my_models' + # end class Configuration attr_accessor :contentful_client_options, :contentful_sync_options, @@ -16,6 +28,7 @@ class Configuration :webhook_user, :webhook_password + # rubocop:disable Metrics/MethodLength def initialize @sync_mode = :poll @contentful_client_options = {} @@ -31,6 +44,7 @@ def initialize @webhook_user = nil @webhook_password = nil end + # rubocop:enable Metrics/MethodLength def initial_sync_options options = { initial: true } diff --git a/lib/consyncful/engine.rb b/lib/consyncful/engine.rb index 137bda8..e022dc6 100644 --- a/lib/consyncful/engine.rb +++ b/lib/consyncful/engine.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true module Consyncful + # Rails engine for Consyncful. + # + # This isolates the Consyncful namespace and allows the gem + # to provide its own routes, controllers, and configuration + # within a Rails application without clashing with the host app. class Engine < ::Rails::Engine isolate_namespace Consyncful end diff --git a/lib/consyncful/sync_runner.rb b/lib/consyncful/sync_runner.rb index a473388..7c38453 100644 --- a/lib/consyncful/sync_runner.rb +++ b/lib/consyncful/sync_runner.rb @@ -1,7 +1,17 @@ # frozen_string_literal: true -# lib/consyncful/sync_runner.rb module Consyncful + # The SyncRunner is responsible for continuously executing Contentful sync + # jobs at a configurable interval or in response to webhook signals. + # + # Modes: + # - :poll — runs the sync every N seconds (default 15) + # - :webhook — waits for webhook signals and triggers a sync when received + # + # Behavior: + # - Starts with an initial sync (`Consyncful::Sync.latest.run`). + # - In poll mode, sleeps for the configured interval and then re-runs sync. + # - In webhook mode, listens for webhook signals and runs sync immediately. class SyncRunner DEFAULT_INTERVAL = 15 VALID_MODES = %i[poll webhook].freeze @@ -17,9 +27,7 @@ def run loop do sleep(@interval) - if @mode == :poll || Consyncful::Sync.consume_webhook_signal! - current_sync.run - end + current_sync.run if @mode == :poll || Consyncful::Sync.consume_webhook_signal! end end diff --git a/spec/controllers/consyncful/webhook_controller_spec.rb b/spec/controllers/consyncful/webhook_controller_spec.rb index 299a7e0..3755918 100644 --- a/spec/controllers/consyncful/webhook_controller_spec.rb +++ b/spec/controllers/consyncful/webhook_controller_spec.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true -require "rails_helper" -RSpec.describe "Consyncful::WebhookController", type: :request do +require 'rails_helper' + +RSpec.describe 'Consyncful::WebhookController', type: :request do let(:config) do # Provide a simple configuration double with the fields used by the controller instance_double( - "Consyncful::Configuration", + 'Consyncful::Configuration', sync_mode: sync_mode, webhook_authentication_required: auth_required, - webhook_user: "user1", - webhook_password: "secret" + webhook_user: 'user1', + webhook_password: 'secret' ) end @@ -17,67 +18,67 @@ allow(Consyncful).to receive(:configuration).and_return(config) end - describe "POST /consyncful/webhook" do - subject(:perform) { post "/consyncful/webhook", headers: headers } + describe 'POST /consyncful/webhook' do + subject(:perform) { post '/consyncful/webhook', headers: headers } let(:headers) { {} } - context "when sync mode is not :webhook" do + context 'when sync mode is not :webhook' do let(:sync_mode) { :poll } let(:auth_required) { false } - it "returns 404 Not Found" do + it 'returns 404 Not Found' do perform expect(response).to have_http_status(:not_found) end end - context "when sync mode is :webhook" do + context 'when sync mode is :webhook' do let(:sync_mode) { :webhook } - context "and authentication is not required" do + context 'and authentication is not required' do let(:auth_required) { false } - it "signals the sync and returns 202 Accepted" do + it 'signals the sync and returns 202 Accepted' do expect(Consyncful::Sync).to receive(:signal_webhook!) perform expect(response).to have_http_status(:accepted) end end - context "and authentication is required" do + context 'and authentication is required' do let(:auth_required) { true } - context "with no credentials" do - it "returns 401 Unauthorized" do + context 'with no credentials' do + it 'returns 401 Unauthorized' do perform expect(response).to have_http_status(:unauthorized) end end - context "with wrong credentials" do + context 'with wrong credentials' do let(:headers) do { - "HTTP_AUTHORIZATION" => - ActionController::HttpAuthentication::Basic.encode_credentials("user1", "wrong") + 'HTTP_AUTHORIZATION' => + ActionController::HttpAuthentication::Basic.encode_credentials('user1', 'wrong') } end - it "returns 401 Unauthorized" do + it 'returns 401 Unauthorized' do perform expect(response).to have_http_status(:unauthorized) end end - context "with correct credentials" do + context 'with correct credentials' do let(:headers) do { - "HTTP_AUTHORIZATION" => - ActionController::HttpAuthentication::Basic.encode_credentials("user1", "secret") + 'HTTP_AUTHORIZATION' => + ActionController::HttpAuthentication::Basic.encode_credentials('user1', 'secret') } end - it "signals the sync and returns 202 Accepted" do + it 'signals the sync and returns 202 Accepted' do expect(Consyncful::Sync).to receive(:signal_webhook!) perform expect(response).to have_http_status(:accepted) diff --git a/spec/internal/config/routes.rb b/spec/internal/config/routes.rb index a067639..33315b0 100644 --- a/spec/internal/config/routes.rb +++ b/spec/internal/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.routes.draw do - mount Consyncful::Engine => "/consyncful" -end \ No newline at end of file + mount Consyncful::Engine => '/consyncful' +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 3b17834..7c1e4e9 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true -ENV["RAILS_ENV"] ||= "test" -require "bundler/setup" -require "combustion" -require "consyncful" -require "consyncful/engine" +ENV['RAILS_ENV'] ||= 'test' + +require 'bundler/setup' +require 'combustion' +require 'consyncful' +require 'consyncful/engine' Combustion.initialize! :action_controller, :active_support -require "rspec/rails" +require 'rspec/rails' RSpec.configure do |config| config.use_active_record = false From 22df2958afabce750ef529c127173a7362d30ce0 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 12:39:48 +1200 Subject: [PATCH 33/37] chore: Adding rspec to the CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a010a5..982f424 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,5 +40,9 @@ jobs: run: | bundle exec rubocop - name: Run tests + run: | + bundle exec rspec + + - name: Run rspec run: | bundle exec rspec \ No newline at end of file From f1a1ede5507d879b9d9c486f6a4e7ee88e6f76d6 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 12:41:06 +1200 Subject: [PATCH 34/37] fix: Remove duplicate step --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 982f424..6d7df5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,7 @@ jobs: - name: Run rubocop run: | bundle exec rubocop - - name: Run tests - run: | - bundle exec rspec - - name: Run rspec + - name: Run tests run: | bundle exec rspec \ No newline at end of file From 6e245890be448ee224a8c3e0e7f07b1ca48515ef Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 12:42:50 +1200 Subject: [PATCH 35/37] chore: Remove test log from git commit --- .gitignore | 2 + spec/internal/log/test.log | 130 ------------------------------------- 2 files changed, 2 insertions(+), 130 deletions(-) delete mode 100644 spec/internal/log/test.log diff --git a/.gitignore b/.gitignore index 559f8e2..b5403a8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ # rspec failure tracking .rspec_status + +/spec/internal/log/ diff --git a/spec/internal/log/test.log b/spec/internal/log/test.log deleted file mode 100644 index 9de5ba0..0000000 --- a/spec/internal/log/test.log +++ /dev/null @@ -1,130 +0,0 @@ -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:40 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:41 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:31:56 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:35:25 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:39:33 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:41:48 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:44:01 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:45:14 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:46:37 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:53:39 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:55:49 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 11:59:22 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:00:41 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:01:37 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:04:07 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:04:21 +1200 - -ActionController::RoutingError (No route matches [POST] "/consyncful/webhook"): - -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:06:32 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 10ms (GC: 6.1ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 404 Not Found in 3ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Filter chain halted as :authenticate rendered or redirected -Completed 401 Unauthorized in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Filter chain halted as :authenticate rendered or redirected -Completed 401 Unauthorized in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:18 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 10ms (GC: 4.6ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Filter chain halted as :authenticate rendered or redirected -Completed 401 Unauthorized in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Filter chain halted as :authenticate rendered or redirected -Completed 401 Unauthorized in 0ms (GC: 0.1ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:44 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 404 Not Found in 2ms (GC: 0.2ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Filter chain halted as :authenticate rendered or redirected -Completed 401 Unauthorized in 0ms (GC: 0.1ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Filter chain halted as :authenticate rendered or redirected -Completed 401 Unauthorized in 0ms (GC: 0.0ms) -Started POST "/consyncful/webhook" for 127.0.0.1 at 2025-08-19 12:08:49 +1200 -Processing by Consyncful::WebhookController#trigger_sync as HTML -Completed 202 Accepted in 0ms (GC: 0.0ms) From 06d4392e42ff3fd2ba43888e0d05a80a508d0e29 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 13:17:08 +1200 Subject: [PATCH 36/37] feat: Increment version --- lib/consyncful/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/consyncful/version.rb b/lib/consyncful/version.rb index f91421b..dd49bd2 100644 --- a/lib/consyncful/version.rb +++ b/lib/consyncful/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Consyncful - VERSION = '1.0.2' + VERSION = '1.1.0' end From 8808030898e637035e22e7141d14845b8a8a6523 Mon Sep 17 00:00:00 2001 From: Daniela Lemow Date: Tue, 19 Aug 2025 13:20:37 +1200 Subject: [PATCH 37/37] feat: Bundle after version increment --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c6caac7..ce2118b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - consyncful (1.0.2) + consyncful (1.1.0) contentful (>= 2.11.1, < 3.0.0) hooks (>= 0.4.1) mongoid (>= 7.0.2)