ActiveRecord query by hashid

697 Views Asked by At

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.

1

There are 1 best solutions below

0
On BEST ANSWER

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"})