ruby-on-railsrails-activerecordhashids

ActiveRecord query by hashid


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.


Solution

  • 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: