ElasticSearch handling Rails route params with nested objects - ruby-on-rails

We have a Rails search route which can accept nested objects that should map to ElasticSearch operators.
For example:
{
name: "John",
age: {
{gte: 20}
}
}
The problem is that the SearchKick library throws an error when the Rails route params look like the following:
{"name"=>["Sam Terrick", "John Terrick"], "age"=>{"gte"=>"20"}}
The Searchkick library maps through these filters and does a case comparison for :gte, but the hash rocket keys do not match. ActiveSupport::HashWithIndifferentAccess doesn't get the job done.
https://github.com/ankane/searchkick/blob/master/lib/searchkick/query.rb
Is there an elegant way to handle this transformation of nested objects from the route params without having to check if each param is a Hash?

For that you could make use of the Rails Hash.html#method-i-deep_transform_keys:
params = {"name"=>["Sam Terrick", "John Terrick"], "age"=>{"gte"=>"20"}}
p params.deep_transform_keys(&:to_sym)
# {:name=>["Sam Terrick", "John Terrick"], :age=>{:gte=>"20"}}
But Rails also implements other handy method, more accurate in this case, Hash.html#deep_symbolize_keys:
p params.deep_symbolize_keys
# # {:name=>["Sam Terrick", "John Terrick"], :age=>{:gte=>"20"}}
Same result.

Related

Writing validations for an array of hashes parameter in rails

I have a new API attribute which is an array of hashes and I would like to validate it as part of built-in rails validation. Since this is a complex object I'm validating I am not finding any valid examples to refer from.
The parameter name is books which are an array of hashes and each hash has three properties genre which should be an enum of three possible values and authors which should be an array of integers and bookId which should be an integer.
Something like this books: [{bookId: 4, genre: "crime", authors: [2, 3, 4]}]
If it's something like an array I can see the documentation for it https://guides.rubyonrails.org/active_record_validations.html here but I am not finding any examples of the above scenarios.
I'm using rails 4.2.1 with ruby 2.3.7 it would be great if you could help me with somewhere to start with this.
For specifically, enum validation I did find a good answer here How do I validate members of an array field?. The trouble is when I need to use this in an array of hashes.
You can write a simple custom validation method yourself. Something like this might be a good start:
validate :format_of_books_array
def format_of_books_array
unless books.is_a?(Array) && books.all? { |b| b.is_a?(Hash) }
errors.add(:books, "is not an array of hashes")
return
end
errors.add(:books, "not all bookIds are integers") unless books.all? { |b| b[:bookId].is_a?(Integer) }
errors.add(:books, "includes invalid genres") unless books.all? { |b| b[:genre].in?(%w[crime romance thriller fantasy]) }
errors.add(:books, "includes invalid author array") unless books.all? { |b| b[:authors].is_a?(Array) && b[:authors].all? { |a| a.is_a?(Integer) } }
end

How to access Chewy results with the dot notation?

I'm using Toptal's Chewy gem to connect and query my Elasticsearch, just like an ODM.
I'm using Chewy along with Elasticsearch 6, Ruby on Rails 5.2 and Active Record.
I've defined my index just like this:
class OrdersIndex < Chewy::Index
define_type Order.includes(:customer) do
field :id, type: "keyword"
field :customer do
field :id, type: "keyword"
field :name, type: "text"
field :email, type: "keyword"
end
end
end
And my model:
class Order < ApplicationRecord
belongs_to :customer
end
The problem here is that when I perform any query using Chewy, the customer data gets deserialized as a hash instead of an Object, and I can't use the dot notation to access the nested data.
results = OrdersIndex.query(query_string: { query: "test" })
results.first.id
# => "594d8e8b2cc640bb78bd115ae644637a1cc84dd460be6f69"
results.first.customer.name
# => NoMethodError: undefined method `name' for #<Hash:0x000000000931d928>
results.first.customer["name"]
# => "Frederique Schaefer"
How can I access the nested association using the dot notation (result.customer.name)? Or to deserialize the nested data inside an Object such as a Struct, that allows me to use the dot notation?
try to use
results = OrdersIndex.query(query_string: { query: "test" }).objects
It converts query result into active record Objects. so dot notation should work. If you want to load any extra association with the above result you can use .load method on Index.
If you want to convert existing ES nested object to accessible with dot notation try to reference this answer. Open Struct is best way to get things done in ruby.
Unable to use dot syntax for ruby hash
also, this one can help too
see this link if you need openStruct to work for nested object
Converting the just-deserialized results to JSON string and deserializing it again with OpenStruct as an object_class can be a bad idea and has a great CPU cost.
I've solved it differently, using recursion and the Ruby's native Struct, preserving the laziness of the Chewy gem.
def convert_to_object(keys, values)
schema = Struct.new(*keys.map(&:to_sym))
object = schema.new(*values)
object.each_pair do |key, value|
if value.is_a?(Hash)
object.send("#{key}=", convert_to_object(value.keys, value.values))
end
end
object
end
OrdersIndex.query(query_string: { query: "test" }).lazy.map do |item|
convert_to_object(item.attributes.keys, item.attributes.values)
end
convert_to_object takes an array of keys and another one of values and creates a struct from it. Whenever the class of one of the array of values items is a Hash, then it converts to a struct, recursively, passing the hash keys and values.
To presence the laziness, that is the coolest part of Chewy, I've used Enumerator::Lazy and Enumerator#map. Mapping every value returned by the ES query into the convert_to_object function, makes every entry a complete struct.
The code is very generic and works to every index I've got.

Rails how to censor some parameters from logs

I'm doing some custom logging in my Rails application and I want to automatically sensor some parameters. I know that we have fitler_parameter_logging.rb which does this for the params object. How can I achieve something like this for my custom hash.
Let's say I'm logging something like this:
Rails.logger.info {name: 'me', secret: '1231234'}.inspect
So my secret key should be sensored in the logs.
I know I can personally delete the key before logging, but it adds noise to my application.
The question title talks about removing the parameters, but your question refers to censoring the parameters similar to how Rails.application.config.filter_parameters works. If it's the latter, it looks like that's already been answered in Manually filter parameters in Rails. If it's the former, assuming a filter list, and a hash:
FILTER_LIST = [:password, :secret]
hash = {'password' => 123, :secret => 321, :ok => "this isn't going anywhere"}
then you could do this:
hash.reject { |k,v| FILTER_LIST.include?(k.to_sym) }
That'll cope with both string and symbol key matching, assuming the filter list is always symbols. Additionally, you could always use the same list as config.filter_parameters if they are going to be the same and you don't need a separate filter list:
hash.reject { |k,v| Rails.application.config.filter_parameters.include?(k.to_sym) }
And if you wanted to make this easier to use within your own logging, you could consider monkey patching the Hash class:
class Hash
def filter_like_parameters
self.reject { |k,v| Rails.application.config.filter_parameters.include?(k.to_sym) }
end
end
Then your logging code would become:
Rails.logger.info {name: 'me', secret: '1231234'}.filter_like_parameters.inspect
If you do monkey patch custom functionality to core classes like that though for calls you're going to be making a lot, it's always best to use a quite obtuse method name to reduce the likelihood of a clash with any other library that might share the same method names.
Hope that helps!

value is updated in DB with ActiveSupport::HashWithIndifferentAccess after serializing

I am trying to update a columns value to a hash in database. The column in database is text.
In model i have,
serialize :order_info
In controller i have update action
def update
Order.update_order_details(update_params, params[:order_info])
head :no_content
end
I am not doing strong parameters for order_info because order_info is an arbitrary hash and after doing research, strong params doesnt support an arbitrary hash
The value that i am trying to pass is like below
"order_info": {
"orders": [
{
"test": "AAAA"
}
],
"detail": "BBBB",
"type": "CCCC"
}
But when i try to update the value it gets updated in database like
--- !ruby/object:ActionController::Parameters parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess comments: - !ruby/hash:ActiveSupport::HashWithIndifferentAccess test: AAAA detail: BBBB type: CCCC permitted: false
serialize is an instance of ActiveSupport::HashWithIndifferentAccess so i am guessing thats why its in the value. How can i get rid of the extra stuff and just update the hash?
If you want to unwrap all the ActionController::Parameters stuff from params[:order_info] without any filtering then the easiest thing to do is call to_unsafe_h (or its alias to_unsafe_hash):
hash = params[:order_info].to_unsafe_h
In Rails4 that should give you a plain old Hash in hash but AFAIK Rails5 will give you an ActiveSupport::HashWithIndifferentAccess so you might want to add a to_h call:
hash = params[:order_info].to_unsafe_h.to_h
The to_h call won't do anything in Rails4 but will give you one less thing to worry about when you upgrade to Rails5.
Then your update call:
Order.update_order_details(
update_params,
params[:order_info].to_unsafe_h.to_h # <-------- Extra DWIM method calls added
)
should give you the YAML in the database that you're looking for:
"---\n:order_info:\n :orders:\n - :test: AAAA\n :detail: BBBB\n :type: CCCC\n"
You might want to throw in a deep_stringify_keys call too:
params[:order_info].to_unsafe_h.to_h.deep_stringify_keys
depending on what sort of keys you want in your YAMLizied Hash.

How to construct URI with query arguments from hash in Ruby

How to construct URI object with query arguments by passing hash?
I can generate query with:
URI::HTTPS.build(host: 'example.com', query: "a=#{hash[:a]}, b=#{[hash:b]}")
which generates
https://example.com?a=argument1&b=argument2
however I think constructing query string for many arguments would be unreadable and hard to maintain. I would like to construct query string by passing hash. Like in example below:
hash = {
a: 'argument1',
b: 'argument2'
#... dozen more arguments
}
URI::HTTPS.build(host: 'example.com', query: hash)
which raises
NoMethodError: undefined method `to_str' for {:a=>"argument1", :b=>"argument2"}:Hash
Is it possible to construct query string based on hash using URI api? I don't want to monkey patch hash object...
For those not using Rails or Active Support, the solution using the Ruby standard library is
hash = {
a: 'argument1',
b: 'argument2'
}
URI::HTTPS.build(host: 'example.com', query: URI.encode_www_form(hash))
=> #<URI::HTTPS https://example.com?a=argument1&b=argument2>
If you have ActiveSupport, just call '#to_query' on hash.
hash = {
a: 'argument1',
b: 'argument2'
#... dozen more arguments
}
URI::HTTPS.build(host: 'example.com', query: hash.to_query)
=> https://example.com?a=argument1&b=argument2
If you are not using rails remember to require 'uri'

Resources