jcypret / hashid-rails Goto Github PK
View Code? Open in Web Editor NEWUse Hashids (http://hashids.org/ruby/) in your Rails app ActiveRecord models.
License: MIT License
Use Hashids (http://hashids.org/ruby/) in your Rails app ActiveRecord models.
License: MIT License
Hey there,
nice to see hashid turning into its own rails gem. But what about form_for
and form_with
helpers in Rails? If i want to obscure ID's within URL's it works fine. Using form_for
would generate its CLASS and ID automatically by using to_key
and reveal the original ID within the DOM. What do you think, is that something that should be addressed?
Need additional specs to cover querying through associations. Current specs are testing against ID, not Hashid.
so you can do params like:
def to_param
"#{hashid}-#{title}"
end
anyone else need this?
ActiveRecord's reload method uses find. When the model's id happens to match the hashid format, the Hashid::Rails.find method 'decodes' the real id and calls super. Super then queries for an id that typically does not exist in the database and can even be too long for an integer column.
The obfuscate_id gem seems to handle this condition by overriding reload (see here)
A spec to reproduce is in a commit here
Hi there,
When I pass an Hashid that decodes to nil, the find method is still called thus an extra unneeded database query. Is this expected behaviour?
Cheers!
I use hashid on most models, but there are a few models I want to use it without overriding to_param because the particular model is used in many places and I dont have time to test if adding hashid will break all those places. How can I tell hashid not to override to_param?
Hi,
When working without any Hashid
configuration, things work properly.
After adding an initializer with this:
# config/initializers/hashids.rb
Hashid::Rails.configure do |config|
config.secret = 'hello'
# config.length = 8
end
I am getting this error:
ActiveRecord::RecordNotFound: Couldn't find SomeModel with an out of range ID
The call that makes it fail is always a fixtures call, like:
task = tasks :some_task
Changing the secret string, makes it fail for different fixtures, and succeed for previously failing fixtures.
A little byebug session to further illustrate:
[43, 52] in /.../task_policy_test.rb
46: test "fotbid change_notification" do
47: byebug
=> 48: refute_permit users(:user), tasks(:patrol), :change_notification?
49: end
(byebug) tasks :patrol # fail
*** ActiveRecord::RecordNotFound Exception: Couldn't find Task with an out of range ID
nil
(byebug) Task.find_by subject: 'Patrol the building' # pass
#<Task id: 201007985, subject: "Patrol the building", ...>
I am attempting to migrate a production app from v0.7.0
to v1.0.0
.
The improvements in 1.0.0
seem very worthwhile and well thought out, and address various other problems - thanks!
The "hashid signing" feature that this gem has implemented (see issue and PR) is causing a totally new set of hashids to be generated.
Would you consider a disable_hashid_signing
config option? I appreciate that v 1.0 is not backward-compatible with 0.x but without this option, it seems there is no way to migrate at all?
Another reason to support disabling this feature is that - although useful, and good to enable by default - it's a totally arbitrary solution for the problem, and it prevents compatibility with other hashid
tools which would not be encoding using the [HASHID_TOKEN, id]
format.
This is probably a very silly question. But how do I encode an ID into the HashID?
I've added the initializer with my own seed. But I can't seem to find out how to do this
More of a question than an issue... I'm just wondering if the database is wiped, is there a way to make the hashes generate the same each time for a given model? Perhaps based on a unique field like a name? I see the salt config but it is more of a global config and only part of the generation picture. Just wondering if there's a way to do that. Thanks!
The default behaviour of Model#find
is to take a single id and returns a single element or take
an array of ids and returns an array of elements (though duplicate ids in the array are not in the result).
using the code below will work with hashid-rails
installed
Model.find([1,2])
=> [#<Model id: 1>, #<Model id: 2>]
Get an error
ActiveRecord::RecordNotFound: Couldn't find Role without an ID
from /Users/drb/.gem/ruby/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/relation/finder_methods.rb:454:in `find_with_ids'
from /Users/drb/.gem/ruby/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/relation/finder_methods.rb:66:in `find'
from /Users/drb/.gem/ruby/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/querying.rb:3:in `find'
from /Users/drb/.gem/ruby/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/core.rb:151:in `find'
from /Users/drb/.gem/ruby/2.3.0/gems/hashid-rails-0.5.0/lib/hashid/rails.rb:66:in `find'
from (irb):1
from /Users/drb/.gem/ruby/2.3.0/gems/railties-5.0.0.1/lib/rails/commands/console.rb:65:in `start'
from /Users/drb/.gem/ruby/2.3.0/gems/railties-5.0.0.1/lib/rails/commands/console_helper.rb:9:in `start'
from /Users/drb/.gem/ruby/2.3.0/gems/railties-5.0.0.1/lib/rails/commands/commands_tasks.rb:78:in `console'
from /Users/drb/.gem/ruby/2.3.0/gems/railties-5.0.0.1/lib/rails/commands/commands_tasks.rb:49:in `run_command!'
from /Users/drb/.gem/ruby/2.3.0/gems/railties-5.0.0.1/lib/rails/commands.rb:18:in `<top (required)>'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:293:in `require'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:293:in `block in require'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:259:in `load_dependency'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:293:in `require'
from /Users/drb/Code/project/mafia-rails/bin/rails:9:in `<top (required)>'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:287:in `load'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:287:in `block in load'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:259:in `load_dependency'
from /Users/drb/.gem/ruby/2.3.0/gems/activesupport-5.0.0.1/lib/active_support/dependencies.rb:287:in `load'
from /Users/drb/.rbenv/versions/2.3.3/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
from /Users/drb/.rbenv/versions/2.3.3/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
from -e:1:in `<main>'
This was found to occur on Ruby on Rails 5.0.1 and ruby 2.3.3 on macOS
Failure/Error: include Hashid::Rails
NameError:
uninitialized constant Hashid
include Hashid::Rails
^^^^^^^^
Did you mean? Hashids
This only happens when using rails
from main
branch.
gem "rails", github: "rails/rails", branch: "main"
gem "activestorage", github: "rails/rails", branch: "main"
gem "hashid-rails", "~> 1.4", ">= 1.4.1"
There are some cases where a standard integer can be decoded as a hashid. A solution to this would be to encode hashids as an array. This is already built in functionality for Hashids (https://github.com/peterhellberg/hashids.rb#encoding-several-numbers). A constant integer (72 = H in ascii) could be set in the first array position and used as a check, then the second would be the actual integer. If the array is not two positions, or if the first position in the array is not 72, then we know the hashid should not be decoded.
HASHID_KEY = 72
#=> 72
id = 518
#=> 518
hashids.encode(HASHID_KEY, id)
#=> "gB0NV05e"
decoded = hashids.decode("gB0NV05e")
#=> [72, 518]
decoded.first == HASHID_KEY && decoded.last == id
#=> true
Hi, thanks for the excellent gem!
Unfortunately, after i upgraded to Rails 6.1 the overriding methods don't work for me any more.
Accessing via .hashid still works.
I don't think i've changed anything else.
Thanks
Seems this gem causes a conflict with paperclip.
Trying to save a Paperclip attachment raises the above error.
Now it's hard coded, it would be better to set hash length in model
Suppose my routes are as follows:
resources :posts do
resources :comments
end
That is to say, a Comment
is nested below the Post
resource.
I have the following URL for viewing a comment on its own page:
/posts/ABC/comments/XYZ
The following works perfectly:
@post = Post.find(params[:post_id])
However, when attempting to retrieve a specific comment, based on the post:
@post = Post.find(params[:post_id])
@comment = Post.comments.find(params[:id])
I see the following error:
ActiveRecord::RecordNotFound at /posts/ABC/comments/XYZ
Couldn't find Comment with 'id'=XYZ [WHERE "comments"."post_id" = 123]
- - -
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1 AND "comments"."id" = $2 LIMIT 1 [["post_id", 123], ["id", 0]]
Another attempt was the following:
@comment = Comment.where(post: @post).find(params[:id])
which errored with:
Couldn't find Comment with 'id'=XYZ [WHERE "comments"."post_id" = 123]
- - -
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 123 AND "comments"."id" = $1 LIMIT 1 [["id", 0]]
Note that in each case the ID is 0
because I'm passing the HashID which is converted to the integer 0
.
It would be great to support this without requiring me to refactor my code and manually handle the HashID <--> ID conversion. Any thoughts?
Hi!
Currently using version 1.0.0 and just noticed that the Model#find_by_hashid method returns my models even if I pass the original integer id values...
If I open the rails console and type Company.find_by_hashid(1)
I get my company with id=1... same thing with other ids... Normally, passing 1 as the argument here should not return the company...
I think just having the Gemfile
installation instructions and not the gem install
would be better. The gem is not really useful outside the context of the app. The existing installation instructions are pretty much boilerplate.
I've been using your gem w/o any issue in my code until I decided to move configuration in an initializer:
/config/initializers/hashid.rb
Hashid::Rails.configure do |config|
# The salt to use for generating hashid. Prepended with table name.
config.salt = "this is my salt"
# The minimum length of generated hashids
config.min_hash_length = 6
# The alphabet to use for generating hashids
config.alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
end
I get following error on startup: Uncaught exception: uninitialized constant Hashid
If you have a has_many association where the parent does not use hashids, find through the association doesn't return the associated record. I added a failing test case to demonstrate.
It seems that to solve this, apart from overriding .find
on the model class, we would have to override ActiveRecord::Associations::CollectionProxy
's #find
method.
Originally posted by @ahawrylak in #55 (comment)
I am attempting to use hashid's with ActiveRecord.where
clauses and it's not working. Is there any way to decode a hashid created this way? Then I could just do Model.where(parent_id: decoded_hash_id)
.
If configured with integers in the HashIDs alphabet, we have run into various problems resulting from the overridden find
method.
Basically, certain hashids are created from numbers only (no letters), and it just so happens that those numbers can be correctly "decoded" into a valid integer.
A specific example for us was a Job which is passed the record's ID, and used find
to retrieve that record. The ID passed in was 197895
but the error returned was Couldn't find Post with 'id'=70459792
. Notice the completely different ID!
It's too late for us to change our HashID alphabet (it would break all existing IDs), but we can workaround this issue by relying on find_by_id
(which is not overridden).
I propose this caveat of the system be prominently added to the README
, as well as defaulting the alphabet to be letters only, to avoid risking this problem for other users.
Assuming I have two classes:
Animal < ApplicationRecord
has_many :dogs
end
Dog < ApplicationRecord
belongs_to :animal
end
I can do
Animal.find(<hashid>)
...but not
Dog.create(animal_id: <hashid>)
Is this intended?
Hi @jcypret , apologies for n00b question. I was hoping to use hashids on both server (Rails-API) and client (Javascript). I am fairly new to hashing and I wasn't sure if I would get reliably consistent results on both server-side and client-side when encoding/decoding the hashids using the JS and this version. Should that be the case?
Thank you!
The fact that the model name is incorporated into the Hashids config is the main reason I rolled my own instead of using this. I'd love to see this so that I could remove some custom code, though I would encourage not tying this configuration concept to "table" and instead just generally allow a different "seed"/"salt" per-model to be mixed in.
My own version of this gem allows for something like:
class Author < ApplicationRecord
# `salt`, `alphabet`, etc fallback to global but `pepper` is required
hashes_ids pepper: 'authors'
end
Originally posted by @bjeanes in #47 (comment)
The current implementation includes the Hashid functionality into ActiveRecord::Base. I would like to instead use an explicit include any any models where needed.
Not all models need this functionality, I think it would be better convert it to model concern. And include when it needed.
Hey - is this considered production ready?
Hi first off, thanks for this gem. I had a question regarding using this with Rails 5. Rails5 requires activerecord 5 (duh) but it looks like this gem depends on activerecord (~> 4.0). This causes Bundler to not find compatible versions for activerecord.
Any way to get this working in Rails5? (I'm new to this process so apologies if I missed something or this isn't the right forum for this).
Thanks!
I'm trying hashid-rails without overriding the find
method. I'd like to be explicit. Unfortunately, to_params
is still overridden so can't use any code like Model.find(model.to_params)
anymore. ActiveAdmin for instance does this and thus breaks.
Could you remove the to_params
alias when config.override_find = false
is set, or make it a separate option (override_to_params
)?
Having trouble with eager loading and using hashid.
@item = Item.includes(:addons, {:addons => :sides}).find_by_hashid(params[:id])
This will work only if params is a hashid, not an integer id. Otherwise "Couldn't find Item"
@item = Item.includes(:addons, {:addons => :sides}).find(params[:id])
This will work only if params is an integer id, not a hashid. Otherwise "record is not found"
Hey,
Trying to see if I can drop this beautiful gem on my project without making too many (or any if possible) changes to it.
At first look, things seemed to be working immediately after adding the gem with zero change.
Only when running my tests I get to a point where this error appears:
Hashids::InputError: unable to unhash
This happens when I try an update
operation on a controller that operates on a special case model. Not sure it is related though, but its the only obvious difference between this failing test and other passing tests.
It is a PeopleController
that updates a User
model.
The error of course originates from the hashids
gem, specifically from this line, but to my understanding it is simply getting an invalid input.
Any thought as to why this happens and maybe a fix?
EDIT:
The immediate suspect is this line with table_name (seems not...)
Currently this commit bumps the version to 1.1.1
- as released on RubyGems.
Master is still showing 1.1.0 because the eager-loading branch isn't fully merged (looks like the changes were cherry-picked without the version bump + changlog commit).
This gem uses the table_name
of the model as the salt for encoding HashIDs. It's great that the hash used differs between models (by depending on the table name), but it would be more secure to use something unique to the server in addition.
What do you think about concatenating a secret server key into the salt, like so:
hashids = Hashids.new("#{table_name}#{ENV['SECRET_KEY_BASE']}")
We should use something more secure; if not the SECRET_KEY_BASE
then something configured via initializers/hashids.rb
or similar?
Update:
I'm considering using ENV['HASHIDS_SALT']
- the reason being, if I ever need to reset my secret_key_base
, I will not invalidate all my HashIDs. I also feel slightly uncomfortable using my secret key as (part of) my salt, because I know HashIDs aren't designed for security, and - although I doubt it's feasible - I can imagine someone reverse engineering my secret_key_base
if they had a set of my HashIDs from which to brute force it.
hi there is there any way for it to return on .select query? example
Person.where(name: "Jason").select(:name, :hashid)
Is there a way to disable find(hashid)
?
I have set up a 6 character hashid with [a-z][0-9]. However, if I do a find for a unhashed id (123456), hashid will hash it and return an ActiveRecord::RecordNotFound exception.
It would be helpful for the find method to either catch the exception and try a non-hashed find or to have a configuration option to disable the find
method.
Not sure what it needs, but the build fails against the ActiveRecord 5.x series.
Rails ActiveRecord has a method called exists? which returns a boolean true
if the record exists or false
if not.
This method now always returns ```false` ' if I give it a hashid instead of a normal ID.
Example code:
context.cart[:items] = context.cart[:items].select do |cart_item_hashid|
Shop::Item.exists?(cart_item_hashid) # always false, Cart is emptied
end
It should generate the following SQL:
SELECT 1 AS one FROM `shop_items` WHERE `shop_items`.`id` = 14 LIMIT 1
The current solution I'm using:
context.cart[:items] = context.cart[:items].select do |cart_item_hashid|
Shop::Item.find(cart_item_hashid).present?
end
This generates the following SQL:
SELECT `shop_items`.* FROM `shop_items` WHERE `shop_items`.`id` = 14 LIMIT 1
This returns the whole record from the DB to check in Ruby code that it is not ```nil` '; it would be much more efficient just to return a boolean.
Another workaround is to do the following:
context.cart[:items] = context.cart[:items].select do |cart_item_hashid|
Shop::Item.exists?(Shop::Item.decode_id(cart_item_hashid))
end
It works fine; however, it is a bit redundant.
Hi, when the model is represented as json, it still has the id from the database rather than the hashid. I'm noob in rails, so I was wondering if there is an easy way to ovveride it for all models?
Hi there,
I resolved this for myself by downgrading back to 0.5 but just figured I'd leave a little note here for anyone who follows.
I have a project in production using HashIDs -- it's how we resolve URLs. Thousands of URLs. Today I upgraded to Rails 5 and in the process upgraded from 0.4.1 to 1.0.0. The config key names changed, I updated them (couldn't find anything in the docs for that but it was pretty simple to figure out).
But even though I kept the values the same (only updated names), old HashIDs just stopped resolving. The secret is the same ,min length is the same, etc -- but find_by_hashid with a valid old hash ID just wouldn't work.
As mentioned, the fix was to drop back to 0.5.0 (so 0.4.1 -> 0.5.0 is fine).
I have been trying with the new hashid_config
, as I have just renamed a table that I has using with hashid-rails
setting the pepper on the model. I love the functionality. Unfortunately it does not work when querying an ActiveRecord_Relation
.
The hashid_config
parameters are set using the model parameters correctly as long as the model is queried directly, eg:
Post.hashid_configuration
=> returns correct pepper that is set in the model
But when if the ActiveRecord_Relation
is in between it does not work, eg:
Post.all.hashid_configuration
=> returns the original table_name as pepper (overriding the one set on the model)
The hashid_config gets called on the ActiveRecord_Relation
, and the options
are empty.
This means that this works
Post.find("hashid")
but this doesn't
Post.includes(:comments).find("hashid")
Hi,
It seems like an old bug I found a year and a half ago (in #16) has resurfaced.
It is now easily reproducible.
When using fixtures, and setting config.sign_hashids
to false
, tests fail with an error like this:
ActiveRecord::RecordNotFound: Couldn't find User without an ID
For convenience, I have created an example repo that demonstrates the problem.
Also, for context, I should probably mention that I am disabling sign_hashids
since I need compatibility with previous version, since I cannot afford my IDs to change (at least in one of my apps).
If any additional information is required, let me know.
The ability to set hashid
to something else would be a nice addition to config.
I ran into this rare issue where I have a model PlaylistTrack
and when I call find(id)
it throws this error: ArgumentError: invalid range "r-0" in string transliteration
.
The error is thrown from the hashids gem with @guards
being "r-0"
https://github.com/peterhellberg/hashids.rb/blob/b729fe514e492e18d9f69473b53eb59a3b5aee4b/lib/hashids.rb#L117
The issue was resolved after adding an explicit pepper to something else instead of the implicit "playlist_tracks"
:
class PlaylistTrack < ApplicationRecord
include Hashid::Rails
hashid_config pepper: "playlisttracks"
...
end
Using this pepper resulted in the @guards
being "y7d"
instead which is accepted.
I guess its just by chance that "playlist_tracks"
produces a problematic pepper but this could potentially be a problem with other table names as well.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.