searchkick 2.3.2 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +377 -84
  3. data/LICENSE.txt +1 -1
  4. data/README.md +859 -602
  5. data/lib/searchkick/bulk_reindex_job.rb +13 -9
  6. data/lib/searchkick/controller_runtime.rb +40 -0
  7. data/lib/searchkick/hash_wrapper.rb +12 -0
  8. data/lib/searchkick/index.rb +281 -356
  9. data/lib/searchkick/index_cache.rb +30 -0
  10. data/lib/searchkick/index_options.rb +487 -281
  11. data/lib/searchkick/indexer.rb +15 -8
  12. data/lib/searchkick/log_subscriber.rb +57 -0
  13. data/lib/searchkick/middleware.rb +9 -2
  14. data/lib/searchkick/model.rb +72 -118
  15. data/lib/searchkick/multi_search.rb +9 -10
  16. data/lib/searchkick/process_batch_job.rb +12 -15
  17. data/lib/searchkick/process_queue_job.rb +22 -13
  18. data/lib/searchkick/query.rb +458 -217
  19. data/lib/searchkick/railtie.rb +7 -0
  20. data/lib/searchkick/record_data.rb +128 -0
  21. data/lib/searchkick/record_indexer.rb +164 -0
  22. data/lib/searchkick/reindex_queue.rb +51 -9
  23. data/lib/searchkick/reindex_v2_job.rb +10 -32
  24. data/lib/searchkick/relation.rb +247 -0
  25. data/lib/searchkick/relation_indexer.rb +155 -0
  26. data/lib/searchkick/results.rb +201 -82
  27. data/lib/searchkick/version.rb +1 -1
  28. data/lib/searchkick/where.rb +11 -0
  29. data/lib/searchkick.rb +269 -97
  30. data/lib/tasks/searchkick.rake +37 -0
  31. metadata +24 -178
  32. data/.gitignore +0 -22
  33. data/.travis.yml +0 -39
  34. data/Gemfile +0 -16
  35. data/Rakefile +0 -20
  36. data/benchmark/Gemfile +0 -23
  37. data/benchmark/benchmark.rb +0 -97
  38. data/lib/searchkick/logging.rb +0 -242
  39. data/lib/searchkick/tasks.rb +0 -33
  40. data/searchkick.gemspec +0 -28
  41. data/test/aggs_test.rb +0 -197
  42. data/test/autocomplete_test.rb +0 -75
  43. data/test/boost_test.rb +0 -202
  44. data/test/callbacks_test.rb +0 -59
  45. data/test/ci/before_install.sh +0 -17
  46. data/test/errors_test.rb +0 -19
  47. data/test/gemfiles/activerecord31.gemfile +0 -7
  48. data/test/gemfiles/activerecord32.gemfile +0 -7
  49. data/test/gemfiles/activerecord40.gemfile +0 -8
  50. data/test/gemfiles/activerecord41.gemfile +0 -8
  51. data/test/gemfiles/activerecord42.gemfile +0 -7
  52. data/test/gemfiles/activerecord50.gemfile +0 -7
  53. data/test/gemfiles/apartment.gemfile +0 -8
  54. data/test/gemfiles/cequel.gemfile +0 -8
  55. data/test/gemfiles/mongoid2.gemfile +0 -7
  56. data/test/gemfiles/mongoid3.gemfile +0 -6
  57. data/test/gemfiles/mongoid4.gemfile +0 -7
  58. data/test/gemfiles/mongoid5.gemfile +0 -7
  59. data/test/gemfiles/mongoid6.gemfile +0 -12
  60. data/test/gemfiles/nobrainer.gemfile +0 -8
  61. data/test/gemfiles/parallel_tests.gemfile +0 -8
  62. data/test/geo_shape_test.rb +0 -175
  63. data/test/highlight_test.rb +0 -78
  64. data/test/index_test.rb +0 -166
  65. data/test/inheritance_test.rb +0 -83
  66. data/test/marshal_test.rb +0 -8
  67. data/test/match_test.rb +0 -276
  68. data/test/misspellings_test.rb +0 -56
  69. data/test/model_test.rb +0 -42
  70. data/test/multi_search_test.rb +0 -36
  71. data/test/multi_tenancy_test.rb +0 -22
  72. data/test/order_test.rb +0 -46
  73. data/test/pagination_test.rb +0 -70
  74. data/test/partial_reindex_test.rb +0 -58
  75. data/test/query_test.rb +0 -35
  76. data/test/records_test.rb +0 -10
  77. data/test/reindex_test.rb +0 -64
  78. data/test/reindex_v2_job_test.rb +0 -32
  79. data/test/routing_test.rb +0 -23
  80. data/test/should_index_test.rb +0 -32
  81. data/test/similar_test.rb +0 -28
  82. data/test/sql_test.rb +0 -214
  83. data/test/suggest_test.rb +0 -95
  84. data/test/support/kaminari.yml +0 -21
  85. data/test/synonyms_test.rb +0 -67
  86. data/test/test_helper.rb +0 -567
  87. data/test/where_test.rb +0 -223
@@ -0,0 +1,7 @@
1
+ module Searckick
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/searchkick.rake"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,128 @@
1
+ module Searchkick
2
+ class RecordData
3
+ TYPE_KEYS = ["type", :type]
4
+
5
+ attr_reader :index, :record
6
+
7
+ def initialize(index, record)
8
+ @index = index
9
+ @record = record
10
+ end
11
+
12
+ def index_data
13
+ data = record_data
14
+ data[:data] = search_data
15
+ {index: data}
16
+ end
17
+
18
+ def update_data(method_name)
19
+ data = record_data
20
+ data[:data] = {doc: search_data(method_name)}
21
+ {update: data}
22
+ end
23
+
24
+ def delete_data
25
+ {delete: record_data}
26
+ end
27
+
28
+ # custom id can be useful for load: false
29
+ def search_id
30
+ id = record.respond_to?(:search_document_id) ? record.search_document_id : record.id
31
+ id.is_a?(Numeric) ? id : id.to_s
32
+ end
33
+
34
+ def document_type(ignore_type = false)
35
+ index.klass_document_type(record.class, ignore_type)
36
+ end
37
+
38
+ def record_data
39
+ data = {
40
+ _index: index.name,
41
+ _id: search_id
42
+ }
43
+ data[:routing] = record.search_routing if record.respond_to?(:search_routing)
44
+ data
45
+ end
46
+
47
+ private
48
+
49
+ def search_data(method_name = nil)
50
+ partial_reindex = !method_name.nil?
51
+
52
+ source = record.send(method_name || :search_data)
53
+
54
+ # conversions
55
+ index.conversions_fields.each do |conversions_field|
56
+ if source[conversions_field]
57
+ source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} }
58
+ end
59
+ end
60
+
61
+ # hack to prevent generator field doesn't exist error
62
+ if !partial_reindex
63
+ index.suggest_fields.each do |field|
64
+ if !source.key?(field) && !source.key?(field.to_sym)
65
+ source[field] = nil
66
+ end
67
+ end
68
+ end
69
+
70
+ # locations
71
+ index.locations_fields.each do |field|
72
+ if source[field]
73
+ if !source[field].is_a?(Hash) && (source[field].first.is_a?(Array) || source[field].first.is_a?(Hash))
74
+ # multiple locations
75
+ source[field] = source[field].map { |a| location_value(a) }
76
+ else
77
+ source[field] = location_value(source[field])
78
+ end
79
+ end
80
+ end
81
+
82
+ if index.options[:inheritance]
83
+ if !TYPE_KEYS.any? { |tk| source.key?(tk) }
84
+ source[:type] = document_type(true)
85
+ end
86
+ end
87
+
88
+ cast_big_decimal(source)
89
+
90
+ source
91
+ end
92
+
93
+ def location_value(value)
94
+ if value.is_a?(Array)
95
+ value.map(&:to_f).reverse
96
+ elsif value.is_a?(Hash)
97
+ {lat: value[:lat].to_f, lon: value[:lon].to_f}
98
+ else
99
+ value
100
+ end
101
+ end
102
+
103
+ # change all BigDecimal values to floats due to
104
+ # https://github.com/rails/rails/issues/6033
105
+ # possible loss of precision :/
106
+ def cast_big_decimal(obj)
107
+ case obj
108
+ when BigDecimal
109
+ obj.to_f
110
+ when Hash
111
+ obj.each do |k, v|
112
+ # performance
113
+ if v.is_a?(BigDecimal)
114
+ obj[k] = v.to_f
115
+ elsif v.is_a?(Enumerable)
116
+ obj[k] = cast_big_decimal(v)
117
+ end
118
+ end
119
+ when Enumerable
120
+ obj.map do |v|
121
+ cast_big_decimal(v)
122
+ end
123
+ else
124
+ obj
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,164 @@
1
+ module Searchkick
2
+ class RecordIndexer
3
+ attr_reader :index
4
+
5
+ def initialize(index)
6
+ @index = index
7
+ end
8
+
9
+ def reindex(records, mode:, method_name:, full: false, single: false)
10
+ # prevents exists? check if records is a relation
11
+ records = records.to_a
12
+ return if records.empty?
13
+
14
+ case mode
15
+ when :async
16
+ unless defined?(ActiveJob)
17
+ raise Error, "Active Job not found"
18
+ end
19
+
20
+ # we could likely combine ReindexV2Job, BulkReindexJob, and ProcessBatchJob
21
+ # but keep them separate for now
22
+ if single
23
+ record = records.first
24
+
25
+ # always pass routing in case record is deleted
26
+ # before the async job runs
27
+ if record.respond_to?(:search_routing)
28
+ routing = record.search_routing
29
+ end
30
+
31
+ Searchkick::ReindexV2Job.perform_later(
32
+ record.class.name,
33
+ record.id.to_s,
34
+ method_name ? method_name.to_s : nil,
35
+ routing: routing,
36
+ index_name: index.name
37
+ )
38
+ else
39
+ Searchkick::BulkReindexJob.perform_later(
40
+ class_name: records.first.class.searchkick_options[:class_name],
41
+ record_ids: records.map { |r| r.id.to_s },
42
+ index_name: index.name,
43
+ method_name: method_name ? method_name.to_s : nil
44
+ )
45
+ end
46
+ when :queue
47
+ if method_name
48
+ raise Error, "Partial reindex not supported with queue option"
49
+ end
50
+
51
+ index.reindex_queue.push_records(records)
52
+ when true, :inline
53
+ index_records, other_records = records.partition { |r| index_record?(r) }
54
+ import_inline(index_records, !full ? other_records : [], method_name: method_name, single: single)
55
+ else
56
+ raise ArgumentError, "Invalid value for mode"
57
+ end
58
+
59
+ # return true like model and relation reindex for now
60
+ true
61
+ end
62
+
63
+ def reindex_items(klass, items, method_name:, single: false)
64
+ routing = items.to_h { |r| [r[:id], r[:routing]] }
65
+ record_ids = routing.keys
66
+
67
+ relation = Searchkick.load_records(klass, record_ids)
68
+ # call search_import even for single records for nested associations
69
+ relation = relation.search_import if relation.respond_to?(:search_import)
70
+ records = relation.select(&:should_index?)
71
+
72
+ # determine which records to delete
73
+ delete_ids = record_ids - records.map { |r| r.id.to_s }
74
+ delete_records =
75
+ delete_ids.map do |id|
76
+ construct_record(klass, id, routing[id])
77
+ end
78
+
79
+ import_inline(records, delete_records, method_name: method_name, single: single)
80
+ end
81
+
82
+ private
83
+
84
+ def index_record?(record)
85
+ record.persisted? && !record.destroyed? && record.should_index?
86
+ end
87
+
88
+ # import in single request with retries
89
+ def import_inline(index_records, delete_records, method_name:, single:)
90
+ return if index_records.empty? && delete_records.empty?
91
+
92
+ maybe_bulk(index_records, delete_records, method_name, single) do
93
+ if index_records.any?
94
+ if method_name
95
+ index.bulk_update(index_records, method_name)
96
+ else
97
+ index.bulk_index(index_records)
98
+ end
99
+ end
100
+
101
+ if delete_records.any?
102
+ index.bulk_delete(delete_records)
103
+ end
104
+ end
105
+ end
106
+
107
+ def maybe_bulk(index_records, delete_records, method_name, single)
108
+ if Searchkick.callbacks_value == :bulk
109
+ yield
110
+ else
111
+ # set action and data
112
+ action =
113
+ if single && index_records.empty?
114
+ "Remove"
115
+ elsif method_name
116
+ "Update"
117
+ else
118
+ single ? "Store" : "Import"
119
+ end
120
+ record = index_records.first || delete_records.first
121
+ name = record.class.searchkick_klass.name
122
+ message = lambda do |event|
123
+ event[:name] = "#{name} #{action}"
124
+ if single
125
+ event[:id] = index.search_id(record)
126
+ else
127
+ event[:count] = index_records.size + delete_records.size
128
+ end
129
+ end
130
+
131
+ with_retries do
132
+ Searchkick.callbacks(:bulk, message: message) do
133
+ yield
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def construct_record(klass, id, routing)
140
+ record = klass.new
141
+ record.id = id
142
+ if routing
143
+ record.define_singleton_method(:search_routing) do
144
+ routing
145
+ end
146
+ end
147
+ record
148
+ end
149
+
150
+ def with_retries
151
+ retries = 0
152
+
153
+ begin
154
+ yield
155
+ rescue Faraday::ClientError => e
156
+ if retries < 1
157
+ retries += 1
158
+ retry
159
+ end
160
+ raise e
161
+ end
162
+ end
163
+ end
164
+ end
@@ -5,28 +5,53 @@ module Searchkick
5
5
  def initialize(name)
6
6
  @name = name
7
7
 
8
- raise Searchkick::Error, "Searchkick.redis not set" unless Searchkick.redis
8
+ raise Error, "Searchkick.redis not set" unless Searchkick.redis
9
9
  end
10
10
 
11
- def push(record_id)
12
- Searchkick.with_redis { |r| r.lpush(redis_key, record_id) }
11
+ # supports single and multiple ids
12
+ def push(record_ids)
13
+ Searchkick.with_redis { |r| r.call("LPUSH", redis_key, record_ids) }
14
+ end
15
+
16
+ def push_records(records)
17
+ record_ids =
18
+ records.map do |record|
19
+ # always pass routing in case record is deleted
20
+ # before the queue job runs
21
+ if record.respond_to?(:search_routing)
22
+ routing = record.search_routing
23
+ end
24
+
25
+ # escape pipe with double pipe
26
+ value = escape(record.id.to_s)
27
+ value = "#{value}|#{escape(routing)}" if routing
28
+ value
29
+ end
30
+
31
+ push(record_ids)
13
32
  end
14
33
 
15
34
  # TODO use reliable queuing
16
35
  def reserve(limit: 1000)
17
- record_ids = Set.new
18
- while record_ids.size < limit && record_id = Searchkick.with_redis { |r| r.rpop(redis_key) }
19
- record_ids << record_id
36
+ if supports_rpop_with_count?
37
+ Searchkick.with_redis { |r| r.call("RPOP", redis_key, limit) }.to_a
38
+ else
39
+ record_ids = []
40
+ Searchkick.with_redis do |r|
41
+ while record_ids.size < limit && (record_id = r.call("RPOP", redis_key))
42
+ record_ids << record_id
43
+ end
44
+ end
45
+ record_ids
20
46
  end
21
- record_ids.to_a
22
47
  end
23
48
 
24
49
  def clear
25
- Searchkick.with_redis { |r| r.del(redis_key) }
50
+ Searchkick.with_redis { |r| r.call("DEL", redis_key) }
26
51
  end
27
52
 
28
53
  def length
29
- Searchkick.with_redis { |r| r.llen(redis_key) }
54
+ Searchkick.with_redis { |r| r.call("LLEN", redis_key) }
30
55
  end
31
56
 
32
57
  private
@@ -34,5 +59,22 @@ module Searchkick
34
59
  def redis_key
35
60
  "searchkick:reindex_queue:#{name}"
36
61
  end
62
+
63
+ def supports_rpop_with_count?
64
+ redis_version >= Gem::Version.new("6.2")
65
+ end
66
+
67
+ def redis_version
68
+ @redis_version ||=
69
+ Searchkick.with_redis do |r|
70
+ info = r.call("INFO")
71
+ matches = /redis_version:(\S+)/.match(info)
72
+ Gem::Version.new(matches[1])
73
+ end
74
+ end
75
+
76
+ def escape(value)
77
+ value.gsub("|", "||")
78
+ end
37
79
  end
38
80
  end
@@ -1,39 +1,17 @@
1
1
  module Searchkick
2
2
  class ReindexV2Job < ActiveJob::Base
3
- RECORD_NOT_FOUND_CLASSES = [
4
- "ActiveRecord::RecordNotFound",
5
- "Mongoid::Errors::DocumentNotFound",
6
- "NoBrainer::Error::DocumentNotFound",
7
- "Cequel::Record::RecordNotFound"
8
- ]
9
-
10
3
  queue_as { Searchkick.queue_name }
11
4
 
12
- def perform(klass, id)
13
- model = klass.constantize
14
- record =
15
- begin
16
- model.find(id)
17
- rescue => e
18
- # check by name rather than rescue directly so we don't need
19
- # to determine which classes are defined
20
- raise e unless RECORD_NOT_FOUND_CLASSES.include?(e.class.name)
21
- nil
22
- end
23
-
24
- index = model.searchkick_index
25
- if !record || !record.should_index?
26
- # hacky
27
- record ||= model.new
28
- record.id = id
29
- begin
30
- index.remove record
31
- rescue Elasticsearch::Transport::Transport::Errors::NotFound
32
- # do nothing
33
- end
34
- else
35
- index.store record
36
- end
5
+ def perform(class_name, id, method_name = nil, routing: nil, index_name: nil)
6
+ model = Searchkick.load_model(class_name, allow_child: true)
7
+ index = model.searchkick_index(name: index_name)
8
+ # use should_index? to decide whether to index (not default scope)
9
+ # just like saving inline
10
+ # could use Searchkick.scope() in future
11
+ # but keep for now for backwards compatibility
12
+ model = model.unscoped if model.respond_to?(:unscoped)
13
+ items = [{id: id, routing: routing}]
14
+ RecordIndexer.new(index).reindex_items(model, items, method_name: method_name, single: true)
37
15
  end
38
16
  end
39
17
  end
@@ -0,0 +1,247 @@
1
+ module Searchkick
2
+ class Relation
3
+ NO_DEFAULT_VALUE = Object.new
4
+
5
+ # note: modifying body directly is not supported
6
+ # and has no impact on query after being executed
7
+ # TODO freeze body object?
8
+ delegate :body, :params, to: :query
9
+ delegate_missing_to :private_execute
10
+
11
+ attr_reader :model
12
+ alias_method :klass, :model
13
+
14
+ def initialize(model, term = "*", **options)
15
+ @model = model
16
+ @term = term
17
+ @options = options
18
+
19
+ # generate query to validate options
20
+ query
21
+ end
22
+
23
+ # same as Active Record
24
+ def inspect
25
+ entries = results.first(11).map!(&:inspect)
26
+ entries[10] = "..." if entries.size == 11
27
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
28
+ end
29
+
30
+ def execute
31
+ Searchkick.warn("The execute method is no longer needed")
32
+ load
33
+ end
34
+
35
+ # experimental
36
+ def limit(value)
37
+ clone.limit!(value)
38
+ end
39
+
40
+ # experimental
41
+ def limit!(value)
42
+ check_loaded
43
+ @options[:limit] = value
44
+ self
45
+ end
46
+
47
+ # experimental
48
+ def offset(value = NO_DEFAULT_VALUE)
49
+ # TODO remove in Searchkick 6
50
+ if value == NO_DEFAULT_VALUE
51
+ private_execute.offset
52
+ else
53
+ clone.offset!(value)
54
+ end
55
+ end
56
+
57
+ # experimental
58
+ def offset!(value)
59
+ check_loaded
60
+ @options[:offset] = value
61
+ self
62
+ end
63
+
64
+ # experimental
65
+ def page(value)
66
+ clone.page!(value)
67
+ end
68
+
69
+ # experimental
70
+ def page!(value)
71
+ check_loaded
72
+ @options[:page] = value
73
+ self
74
+ end
75
+
76
+ # experimental
77
+ def per_page(value = NO_DEFAULT_VALUE)
78
+ # TODO remove in Searchkick 6
79
+ if value == NO_DEFAULT_VALUE
80
+ private_execute.per_page
81
+ else
82
+ clone.per_page!(value)
83
+ end
84
+ end
85
+
86
+ # experimental
87
+ def per_page!(value)
88
+ check_loaded
89
+ @options[:per_page] = value
90
+ self
91
+ end
92
+
93
+ # experimental
94
+ def where(value = NO_DEFAULT_VALUE)
95
+ if value == NO_DEFAULT_VALUE
96
+ Where.new(self)
97
+ else
98
+ clone.where!(value)
99
+ end
100
+ end
101
+
102
+ # experimental
103
+ def where!(value)
104
+ check_loaded
105
+ if @options[:where]
106
+ @options[:where] = {_and: [@options[:where], ensure_permitted(value)]}
107
+ else
108
+ @options[:where] = ensure_permitted(value)
109
+ end
110
+ self
111
+ end
112
+
113
+ # experimental
114
+ def rewhere(value)
115
+ clone.rewhere!(value)
116
+ end
117
+
118
+ # experimental
119
+ def rewhere!(value)
120
+ check_loaded
121
+ @options[:where] = ensure_permitted(value)
122
+ self
123
+ end
124
+
125
+ # experimental
126
+ def order(*values)
127
+ clone.order!(*values)
128
+ end
129
+
130
+ # experimental
131
+ def order!(*values)
132
+ values = values.first if values.size == 1 && values.first.is_a?(Array)
133
+ check_loaded
134
+ (@options[:order] ||= []).concat(values)
135
+ self
136
+ end
137
+
138
+ # experimental
139
+ def reorder(*values)
140
+ clone.reorder!(*values)
141
+ end
142
+
143
+ # experimental
144
+ def reorder!(*values)
145
+ check_loaded
146
+ @options[:order] = values
147
+ self
148
+ end
149
+
150
+ # experimental
151
+ def select(*values, &block)
152
+ if block_given?
153
+ private_execute.select(*values, &block)
154
+ else
155
+ clone.select!(*values)
156
+ end
157
+ end
158
+
159
+ # experimental
160
+ def select!(*values)
161
+ check_loaded
162
+ (@options[:select] ||= []).concat(values)
163
+ self
164
+ end
165
+
166
+ # experimental
167
+ def reselect(*values)
168
+ clone.reselect!(*values)
169
+ end
170
+
171
+ # experimental
172
+ def reselect!(*values)
173
+ check_loaded
174
+ @options[:select] = values
175
+ self
176
+ end
177
+
178
+ # experimental
179
+ def includes(*values)
180
+ clone.includes!(*values)
181
+ end
182
+
183
+ # experimental
184
+ def includes!(*values)
185
+ check_loaded
186
+ (@options[:includes] ||= []).concat(values)
187
+ self
188
+ end
189
+
190
+ # experimental
191
+ def only(*keys)
192
+ Relation.new(@model, @term, **@options.slice(*keys))
193
+ end
194
+
195
+ # experimental
196
+ def except(*keys)
197
+ Relation.new(@model, @term, **@options.except(*keys))
198
+ end
199
+
200
+ # experimental
201
+ def load
202
+ private_execute
203
+ self
204
+ end
205
+
206
+ def loaded?
207
+ !@execute.nil?
208
+ end
209
+
210
+ def respond_to_missing?(method_name, include_all)
211
+ Results.new(nil, nil, nil).respond_to?(method_name, include_all) || super
212
+ end
213
+
214
+ # TODO uncomment in 6.0
215
+ # def to_yaml
216
+ # private_execute.to_a.to_yaml
217
+ # end
218
+
219
+ private
220
+
221
+ def private_execute
222
+ @execute ||= query.execute
223
+ end
224
+
225
+ def query
226
+ @query ||= Query.new(@model, @term, **@options)
227
+ end
228
+
229
+ def check_loaded
230
+ raise Error, "Relation loaded" if loaded?
231
+
232
+ # reset query since options will change
233
+ @query = nil
234
+ end
235
+
236
+ # provides *very* basic protection from unfiltered parameters
237
+ # this is not meant to be comprehensive and may be expanded in the future
238
+ def ensure_permitted(obj)
239
+ obj.to_h
240
+ end
241
+
242
+ def initialize_copy(other)
243
+ super
244
+ @execute = nil
245
+ end
246
+ end
247
+ end