diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d580b66e..8560fb10 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2023-11-06 11:54:44 UTC using RuboCop version 1.27.0. +# on 2024-01-10 10:49:28 UTC using RuboCop version 1.27.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -30,6 +30,15 @@ Layout/EmptyLinesAroundModuleBody: Exclude: - 'lib/meilisearch-rails.rb' +# Offense count: 3 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineMethodCallBraceLayout: + Exclude: + - 'spec/integration_spec.rb' + - 'spec/ms_clean_up_job_spec.rb' + # Offense count: 1 # This cop supports safe auto-correction (--auto-correct). # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -44,6 +53,13 @@ Lint/SuppressedException: Exclude: - 'lib/meilisearch-rails.rb' +# Offense count: 1 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'lib/meilisearch-rails.rb' + # Offense count: 2 # This cop supports safe auto-correction (--auto-correct). # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. @@ -54,7 +70,7 @@ Lint/UnusedMethodArgument: # Offense count: 11 # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 102 + Max: 104 # Offense count: 1 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. @@ -75,17 +91,17 @@ Metrics/CyclomaticComplexity: # Offense count: 16 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: - Max: 99 + Max: 103 # Offense count: 1 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 435 + Max: 449 # Offense count: 8 # Configuration parameters: IgnoredMethods. Metrics/PerceivedComplexity: - Max: 33 + Max: 34 # Offense count: 1 Naming/AccessorMethodName: @@ -107,7 +123,7 @@ Naming/MethodParameterName: Exclude: - 'lib/meilisearch-rails.rb' -# Offense count: 15 +# Offense count: 20 RSpec/BeforeAfterAll: Exclude: - 'spec/integration_spec.rb' @@ -118,7 +134,7 @@ RSpec/DescribeClass: Exclude: - 'spec/integration_spec.rb' -# Offense count: 37 +# Offense count: 46 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 19 @@ -132,7 +148,7 @@ RSpec/FilePath: - 'spec/settings_spec.rb' - 'spec/utilities_spec.rb' -# Offense count: 26 +# Offense count: 25 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: @@ -150,6 +166,12 @@ RSpec/MultipleDescribes: Exclude: - 'spec/integration_spec.rb' +# Offense count: 1 +# This cop supports safe auto-correction (--auto-correct). +RSpec/MultipleSubjects: + Exclude: + - 'spec/ms_clean_up_job_spec.rb' + # Offense count: 1 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: @@ -188,14 +210,45 @@ Style/InverseMethods: Exclude: - 'lib/meilisearch-rails.rb' -# Offense count: 8 +# Offense count: 1 +# This cop supports safe auto-correction (--auto-correct). +Style/MultilineIfModifier: + Exclude: + - 'lib/meilisearch-rails.rb' + +# Offense count: 1 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: AllowedMethods. +# AllowedMethods: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with +Style/NestedParenthesizedCalls: + Exclude: + - 'spec/ms_clean_up_job_spec.rb' + +# Offense count: 9 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - 'lib/meilisearch-rails.rb' -# Offense count: 20 +# Offense count: 13 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'spec/integration_spec.rb' + - 'spec/ms_clean_up_job_spec.rb' + +# Offense count: 2 +# This cop supports safe auto-correction (--auto-correct). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Exclude: + - 'spec/integration_spec.rb' + +# Offense count: 19 # This cop supports safe auto-correction (--auto-correct). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/lib/meilisearch-rails.rb b/lib/meilisearch-rails.rb index af8117b7..a976d088 100644 --- a/lib/meilisearch-rails.rb +++ b/lib/meilisearch-rails.rb @@ -249,6 +249,7 @@ def additional_indexes # lazy load the ActiveJob class to ensure the # queue is initialized before using it autoload :MSJob, 'meilisearch/rails/ms_job' + autoload :MSCleanUpJob, 'meilisearch/rails/ms_clean_up_job' end # this class wraps an MeiliSearch::Index document ensuring all raised exceptions @@ -382,7 +383,11 @@ def meilisearch(options = {}, &block) proc = if options[:enqueue] == true proc do |record, remove| - MSJob.perform_later(record, remove ? 'ms_remove_from_index!' : 'ms_index!') + if remove + MSCleanUpJob.perform_later(record.ms_entries) + else + MSJob.perform_later(record, 'ms_index!') + end end elsif options[:enqueue].respond_to?(:call) options[:enqueue] @@ -454,7 +459,7 @@ def meilisearch(options = {}, &block) end end elsif respond_to?(:after_destroy) - after_destroy { |searchable| searchable.ms_enqueue_remove_from_index!(ms_synchronous?) } + after_destroy_commit { |searchable| searchable.ms_enqueue_remove_from_index!(ms_synchronous?) } end end @@ -563,6 +568,19 @@ def ms_index!(document, synchronous = false) end.compact end + def ms_entries_for(document:, synchronous:) + primary_key = ms_primary_key_of(document) + raise ArgumentError, 'Cannot index a record without a primary key' if primary_key.blank? + + ms_configurations.filter_map do |options, settings| + { + synchronous: synchronous || options[:synchronous], + index_uid: options[:index_uid], + primary_key: primary_key + }.with_indifferent_access unless ms_indexing_disabled?(options) + end + end + def ms_remove_from_index!(document, synchronous = false) return if ms_without_auto_index_scope @@ -940,6 +958,10 @@ def ms_synchronous? @ms_synchronous end + def ms_entries(synchronous = false) + self.class.ms_entries_for(document: self, synchronous: synchronous || ms_synchronous?) + end + private def ms_mark_synchronous diff --git a/lib/meilisearch/rails/ms_clean_up_job.rb b/lib/meilisearch/rails/ms_clean_up_job.rb new file mode 100644 index 00000000..f52fc935 --- /dev/null +++ b/lib/meilisearch/rails/ms_clean_up_job.rb @@ -0,0 +1,19 @@ +module MeiliSearch + module Rails + class MSCleanUpJob < ::ActiveJob::Base + queue_as :meilisearch + + def perform(documents) + documents.each do |document| + index = MeiliSearch::Rails.client.index(document[:index_uid]) + + if document[:synchronous] + index.delete_document!(document[:primary_key]) + else + index.delete_document(document[:primary_key]) + end + end + end + end + end +end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 3eb3a78c..f035a2c5 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -628,6 +628,34 @@ expect(results.size).to eq(1) end + describe '#ms_entries' do + it 'returns all 3 indexes for a public book' do + book = Book.create!( + name: 'Frankenstein', author: 'Mary Shelley', + premium: false, released: true + ) + + expect(book.ms_entries).to contain_exactly( + a_hash_including("index_uid" => safe_index_uid('SecuredBook')), + a_hash_including("index_uid" => safe_index_uid('BookAuthor')), + a_hash_including("index_uid" => safe_index_uid('Book')), + ) + end + + it 'returns all 3 indexes for a non-public book' do + book = Book.create!( + name: 'Frankenstein', author: 'Mary Shelley', + premium: false, released: false + ) + + expect(book.ms_entries).to contain_exactly( + a_hash_including("index_uid" => safe_index_uid('SecuredBook')), + a_hash_including("index_uid" => safe_index_uid('BookAuthor')), + a_hash_including("index_uid" => safe_index_uid('Book')), + ) + end + end + it 'returns facets using max values per facet' do 10.times do Book.create! name: Faker::Book.title, author: Faker::Book.author, genre: Faker::Book.genre @@ -932,7 +960,10 @@ end describe 'ConditionallyEnqueuedDocument' do - before { allow(MeiliSearch::Rails::MSJob).to receive(:perform_later).and_return(nil) } + before do + allow(MeiliSearch::Rails::MSJob).to receive(:perform_later).and_return(nil) + allow(MeiliSearch::Rails::MSCleanUpJob).to receive(:perform_later).and_return(nil) + end it 'does not try to enqueue an index job when :if option resolves to false' do doc = ConditionallyEnqueuedDocument.create! name: 'test', is_public: false @@ -952,7 +983,7 @@ doc.destroy! - expect(MeiliSearch::Rails::MSJob).to have_received(:perform_later).with(doc, 'ms_remove_from_index!') + expect(MeiliSearch::Rails::MSCleanUpJob).to have_received(:perform_later).with(doc.ms_entries) end end end @@ -1049,6 +1080,19 @@ expect(cat_index).to eq(dog_index) end + + describe '#ms_entries' do + it 'returns the correct entry for each animal' do + toby_dog = Dog.create!(name: 'Toby the Dog') + taby_cat = Cat.create!(name: 'Taby the Cat') + + expect(toby_dog.ms_entries).to contain_exactly( + a_hash_including('primary_key' => /dog_\d+/)) + + expect(taby_cat.ms_entries).to contain_exactly( + a_hash_including('primary_key' => /cat_\d+/)) + end + end end describe 'Songs' do diff --git a/spec/ms_clean_up_job_spec.rb b/spec/ms_clean_up_job_spec.rb new file mode 100644 index 00000000..ccbf687c --- /dev/null +++ b/spec/ms_clean_up_job_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +RSpec.describe 'MeiliSearch::Rails::MSCleanUpJob' do + include ActiveJob::TestHelper + + def clean_up_indexes + indexes.each(&:delete_all_documents) + end + + def create_indexed_record + record + + indexes.each do |index| + index.wait_for_task(index.tasks['results'].last['uid']) + end + end + + subject(:clean_up) { MeiliSearch::Rails::MSCleanUpJob } + + let(:record) do + Book.create name: "Moby Dick", author: "Herman Mellville", + premium: false, released: true + end + + let(:record_entries) do + record.ms_entries(true).each { |h| h[:index_uid] += '_test' } + end + + let(:indexes) do + %w[SecuredBook BookAuthor Book].map do |uid| + Book.index(safe_index_uid uid) + end + end + + it 'removes record from all indexes' do + clean_up_indexes + + create_indexed_record + + clean_up.perform_now(record_entries) + + indexes.each do |index| + expect(index.search('*')['hits']).to be_empty + end + end + + context 'when record is already destroyed' do + subject(:record) do + Restaurant.create( + name: "Los Pollos Hermanos", + kind: "Mexican", + description: "Mexican chicken restaurant in Albuquerque, New Mexico.") + end + + let(:indexes) { [Restaurant.index] } + + it 'successfully deletes its document in the index' do + clean_up_indexes + + create_indexed_record + + record.delete # does not run callbacks, unlike #destroy + + clean_up.perform_later(record_entries) + expect { perform_enqueued_jobs }.not_to raise_error + + indexes.each do |index| + expect(index.search('*')['hits']).to be_empty + end + end + end +end diff --git a/spec/ms_job_spec.rb b/spec/ms_job_spec.rb index 4fc4a1e3..12dc33f7 100644 --- a/spec/ms_job_spec.rb +++ b/spec/ms_job_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' RSpec.describe 'MeiliSearch::Rails::MSJob' do + include ActiveJob::TestHelper + subject(:job) { MeiliSearch::Rails::MSJob } let(:record) { double } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6074150b..0683e8eb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,6 +31,11 @@ require 'active_model_serializers' require 'byebug' +# Required for running background jobs on demand (deterministically) +ActiveJob::Base.queue_adapter = :test +# Required for serializing objects in similar to production environments +GlobalID.app = 'meilisearch-test' + OLD_RAILS = Gem.loaded_specs['rails'].version < Gem::Version.new('4.0') NEW_RAILS = Gem.loaded_specs['rails'].version >= Gem::Version.new('6.0') diff --git a/spec/support/2_method_helpers.rb b/spec/support/2_method_helpers.rb index 4f8e4997..e8aa332f 100644 --- a/spec/support/2_method_helpers.rb +++ b/spec/support/2_method_helpers.rb @@ -1,16 +1,16 @@ # A unique prefix for your test run in local or CI SAFE_INDEX_PREFIX = "rails_#{SecureRandom.hex(8)}".freeze -def indexes +def _indexes @indexes ||= {} end # avoid concurrent access to the same index in local or CI def safe_index_uid(name) - indexes[name] ||= "#{SAFE_INDEX_PREFIX}_#{name}" + _indexes[name] ||= "#{SAFE_INDEX_PREFIX}_#{name}" end # get a list of safe indexes in local or CI def safe_index_list - indexes.values.flat_map { |safe_idx| [safe_idx, "#{safe_idx}_test"] } + _indexes.values.flat_map { |safe_idx| [safe_idx, "#{safe_idx}_test"] } end diff --git a/spec/support/active_record_classes.rb b/spec/support/active_record_classes.rb index f77e0ee8..4a661cc4 100644 --- a/spec/support/active_record_classes.rb +++ b/spec/support/active_record_classes.rb @@ -165,7 +165,9 @@ class Camera < Product end class Restaurant < ActiveRecord::Base + include GlobalID::Identification include MeiliSearch::Rails + meilisearch index_uid: safe_index_uid('Restaurant') do attributes_to_crop [:description] crop_length 10