We use https://github.com/peterhellberg/hashids.rb to obfuscate database IDs within our API:
HASHID = Hashids.new("this is my salt")
product_id = 12345
hash = HASHID.encode(product_id)
=> "NkK9"
When decoding hashids, we have to do something like this:
Product.find_by(id: HASHID.decode(params[:hashid]))
And this pattern repeats a lot in our application. I could write some helper functions like find_by_hashid
or where_hashid
that take care of the decoding and possible error handling. But when combining them with other query methods, this quickly becomes brittle.
So I was wondering, if it is possible to extend the ActiveRecord query interface to support a special virtual column hashid
, so that stuff like this is possible:
Product.where(hashid: ["Nkk9", "69PV"])
Product.where.not(hashid: "69PV")
Product.find_by(hashid: "Nkk9")
Product.find("Nkk9")
# store has_many :products
store.products.where(hashid: "69PV")
The idea is pretty simple, just look for the hashid
key, turn it into id
and decode the given hashid string. On error, return nil
.
But I'm not sure if ActiveRecord provides a way to do this, without a lot of monkey patching.
You might be able to hack in this basic options as follows but I wouldn't ever recommend it:
module HashIDable
module Findable
def find(*args,&block)
args = args.flatten.map! do |arg|
next arg unless arg.is_a?(String)
decoded = ::HASHID.decode(arg)
::HASHID.encode(decoded.to_i) == arg ? decoded : arg
end
super(*args,&block)
end
end
module Whereable
def where(*args)
args.each do |arg|
if arg.is_a?(Hash) && arg.key?(:hashid)
arg.merge!(id: ::HASHID.decode(arg.delete(:hashid).to_s))
end
end
super(*args)
end
end
end
ActiveRecord::FinderMethods.prepend(HashIDable::Findable)
ActiveRecord::QueryMethods.prepend(HashIDable::Whereable)
You could place this file in "config/initializers" and see what happens but this implementation is extremely naive and very tenuous.
There are likely 101 places where the above will fail to be accounted for including, but definitely not limited to:
MyModel.where("hashid = ?", "Nkk9")
MyModel.joins(:other_model).where(other_models: {hashid: "Nkk9"})