GithubHelp home page GithubHelp logo

hult / acts_as_having_string_id Goto Github PK

View Code? Open in Web Editor NEW
9.0 3.0 2.0 90 KB

A Rails plugin for exposing non-sequential (Youtube-like) string IDs instead of the sequential integer IDs provided by Rails

License: MIT License

Ruby 85.87% JavaScript 2.29% CSS 1.48% HTML 10.36%
rails tiny-encryption-algorithm id rails-plugins

acts_as_having_string_id's Introduction

ActsAsHavingStringId

codecov

A Rails plugin for exposing non-sequential (Youtube-like) string IDs instead of the sequential integer IDs provided by Rails.

Before, your API may look like

GET /users/123
{
  "id": 123,
  "name": "Alice O'User"
}

After

GET /users/9w63Hubh4oL
{
  "id": "9w63Hubh4oL",
  "name": "Alice O'User"
}

Problem

Exposing sequential integer IDs has several drawbacks:

Why not use UUIDs?

"But why not just use UUIDs", you ask? Rails has built-in support for them. But they are very long. Exposing them in an API is okay, but in a URL just doesn't look nice

http://example.com/objects/be398f64-320f-4731-be73-74699e6795bc

Even base62 encoding that ID is very long

http://example.com/objects/27WzQMxpvINgio2w5Xt0hk

64-bit integers would be optimal, but they can't be random: the risk of collisions would be too high.

Our solution

Rails makes heavy use of sequential integer IDs internally, but there's no need of exposing them. ActsAsHavingStringId provides an alternative string representation of your IDs. This representation is

base62(tea(id, md5(ModelClass.name + Rails.application.secrets.string_id_key)))

The representation looks something like "E0znqip4mRA".

tea above is the "New variant" of the Tiny Encryption Algorithm. You should probably not consider your id to be forever secret, but it should be pretty hard to figure out from the string representation.

Your controllers will continue to work without modification, but will start to accept string IDs. So if http://example.com/orders/104 worked before, something like http://example.com/orders/E0znqip4mRA should magically work.

Usage

First, set up your secrets.yml:

development:
  string_id_key: notverysecret

test:
  string_id_key: notverysecreteither

production:
  string_id_key: <%= ENV["STRING_ID_KEY"] %>

Then, call the method in your model class, after any relations to other models:

class MyModel < ApplicationRecord
  has_many :my_other_model
  acts_as_having_string_id
end

The id of your model will now not be an int, but rather an instance of ActsAsHavingStringId::StringId. As an example:

> m = MyModel.create!
> m.id
=> 1/7EajpSfdWIf
> m.id.to_i
=> 1
> m.id.to_s
=> "7EajpSfdWIf"

All ActiveRecord functions will continue to accept int IDs, but will now also accept the string representation as input:

> MyModel.find("7EajpSfdWIf")
=> #<MyModel id: 1/7EajpSfdWIf, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">
> MyModel.where(id: "7EajpSfdWIf")
=> #<ActiveRecord::Relation [#<MyModel id: 1/7EajpSfdWIf, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">]>

In all associated models, foreign keys to your model will also be this new type of id.

> MyOtherModel.create! my_model: MyModel.first
=> #<MyOtherModel id: 1, my_model_id: 1/GBpjdLndSR0, created_at: "2016-09-07 10:32:24", updated_at: "2016-09-07 10:32:24">

Then, for exposing your string ID, make sure to always use id.to_s. For example, if you're using ActiveModelSerializers:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name

  def id
    object.id.to_s
  end
end

You can get the string representation of an ID from a class without having the instance

> MyModel.id_string(1)
=> "7EajpSfdWIf"

And, conversely, getting the ID from the string representation

> MyModel.id_int("7EajpSfdWIf")
=> 1

And that's just about it!

But I'm getting these weird type cast errors

If you have has_many :through relations in your app, you may need to add a few associations more in your app in order for Rails to understand how your data model fits together. For example in this model

class Author < ApplicationRecord
end

class Blog < ApplicationRecord
  has_many :posts
  has_many :authors, through: :posts
  acts_as_having_string_id
end

class Post < ApplicationRecord
  belongs_to :blog
  belongs_to :author
end

an attempt to do Blog.first.authors will raise a TypeError: can't cast ActsAsHavingStringId::StringId.

In order to fix this, you'll need to add the missing association has_many :posts to Author.

TODO

  • Since the MyModel.find("7EajpSfdWIf") functionality depends on the argument now being a string, MyModel.find("5") will no longer mean MyModel.find(5), but rather MyModel.find(4387534) or something. Is that a problem?
  • It's a potential security problem that we don't force strings from controllers (integer id coming from JSON postdata will make it find by original id)

Installation

Add this line to your application's Gemfile:

gem 'acts_as_having_string_id'

And then execute:

$ bundle

Or install it yourself as:

$ gem install acts_as_having_string_id

Contributing

To contribute, fork and clone the repo, edit the code (don't change the version number of the gem). Add tests, run them using

bin/test

Then create a pull request.

To build the gem (this is mostly for myself), run

gem build acts_as_having_string_id.gemspec

Acknowledgements

The Tiny Encryption Algorithm was created by David Wheeler and Roger Needham of the Cambridge Computer Laboratory. This library's implementation is based on this code by Jeremy Hinegardner.

License

The gem is available as open source under the terms of the MIT License.

acts_as_having_string_id's People

Contributors

clairity avatar dependabot[bot] avatar hult avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

acts_as_having_string_id's Issues

Railtie initialization doesn't happen when Spring reloads

How to reproduce:

  • In an application using acts_as_having_string_id with working tests, run the tests
  • Note that they're not failing
  • Touch a file to make spring reload, something like touch app/models/application_record.rb
  • Re-run the tests

What happens:

The tests fail with an error like

Error:
SomethingTest#test_something:
NameError: undefined local variable or method `acts_as_having_string_id' for #<Class:0x007fd80cff3980>
app/models/something.rb:2:in `<class:Something>'
app/models/something.rb:1:in `<top (required)>'`

What I expect to happen:

The tests should work.

A workaround is to include ActsAsHavingStringId in your ApplicationRecord.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In snapshot (Gemfile.lock):
    url (>= 0.3.2)

  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Change gemspec to allow rails 5.1 betas

Currently acts_as_having_string_id.gemspec specifies

s.add_dependency "rails", "~> 5.0.0", ">= 5.0.0.1"

any chance this dependency could be changed to something like "~> 5.0", ">= 5.0.0.1" so that we could use acts_as_having_string_id with the 5.1.0.beta releases? I'm getting a bundler dependency resolution error when i try to update to rails 5.1.0.beta1.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Finding by an invalid string id gives wrong error in Postgres

If you have a postgres backend to your activerecord, and you find by an invalid string id, you'll get the wrong error

> Deck.find("fdsfsdkhks")
  Deck Load (26.2ms)  SELECT  "decks".* FROM "decks" WHERE "decks"."id" = $1 LIMIT $2  [["id", 844233448306812871], ["LIMIT", 1]]
ActiveRecord::StatementInvalid: PG::NumericValueOutOfRange: ERROR:  value "844233448306812871" is out of range for type integer

I expect something like

> Deck.find(12345)
  Deck Load (0.9ms)  SELECT  "decks".* FROM "decks" WHERE "decks"."id" = $1 LIMIT $2  [["id", 12345], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find Deck with 'id'=12345

I think the solution may just be to remove the implementation of StringId::Type.type (that currently says :integer)

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In snapshot (Gemfile.lock):
    url (>= 0.3.2)

  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

constant ::Fixnum is deprecated warning with ruby 2.4.x

Ruby 2.4.x causes acts_as_having_string_id to display this warning on the console:

~/.rvm/gems/ruby-2.4.1@app/gems/acts_as_having_string_id-0.2.4/lib/acts_as_having_string_id/string_id.rb:60: warning: constant ::Fixnum is deprecated

due to unification of Fixnum and Bignum into Integer in Ruby 2.4.0: https://bugs.ruby-lang.org/issues/12005

From some quick testing based on http://blog.bigbinary.com/2016/11/18/ruby-2-4-unifies-fixnum-and-bignum-into-integer.html, I found that changing line 60 in string_id.rb to this:

if value.is_a?(String) || value.is_a?(1.class)

in place of

if value.is_a?(String) || value.is_a?(Fixnum)

makes the warning go away.

Renaming a class changes all its IDs

If you have a class like this

class A < ApplicationRecord
  acts_as_having_string_id
end

An instance with id i is going to have string_id set as

ActsAsHavingStringId::TEA.new("A" + Rails.application.secrets.string_id_key).encrypt(i).base62_encode

If you later rename that model class to B, the instance's id will change to

ActsAsHavingStringId::TEA.new("B" + Rails.application.secrets.string_id_key).encrypt(i).base62_encode

Which almost certainly won't be the same.

I suggest adding a class method id_string_base_name which by default returns name, but that can be overridden in case the class name changes.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In snapshot (Gemfile.lock):
    url (>= 0.3.2)

  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In snapshot (Gemfile.lock):
    url (>= 0.3.2)

  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

`has_many :through` doesn't work

Provided a setup like

class A < ApplicationRecord
  has_many :bs
  has_many :cs, through :bs
end

class B < ApplicationRecord
  belongs_to :a
  has_many :cs
end

class C < ApplicationRecord
  belongs_to :b
end

Getting A.first.cs will fail with an error.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In snapshot (Gemfile.lock):
    url (>= 0.3.2)

  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In snapshot (Gemfile.lock):
    url (>= 0.3.2)

  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Attributes are generated for too many associations

The idea behind ActsAsHavingStringId::acts_as_having_string_id is to make the id column of the class in which you're calling it be of type ActsAsHavingStringId::StringId::Type, and to do the same with all columns that are foreign keys to that class.

But now, if you have this setup

class A < ApplicationRecord
  has_many :bs
  acts_as_having_string_id
end

class B < ApplicationRecord
  belongs_to :a
  acts_as_having_string_id
end

You will end up with an attribute A#a_id (returning nil). In some cases, haven't figured out when, this method will also show in A#inspect.

The current condition when looping through the reflections is (acts_as_having_string_id.rb:16)

self.reflections.each_value do |r|
  unless r.is_a? ActiveRecord::Reflection::ThroughReflection
    r.klass.class_eval do
      attribute r.foreign_key.to_sym, attrib_type
    end
  end
end

But this is too generous. From what I've been able to figure out, the condition should rather be

if r.is_a?(ActiveRecord::Reflection::HasManyReflection) || \
  (r.is_a?(ActiveRecord::Reflection::BelongsToReflection) && self == r.klass)

Only then is it the case that r.klass has an attribute that is a foreign key to self.

Also, I'm not quite sure what's the deal with HasAndBelongsToManyReflection and HasOneReflection.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In snapshot (Gemfile.lock):
    url (>= 0.3.2)

  In Gemfile:
    codecov was resolved to 0.1.13, which depends on
      url

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

NumericValueOutOfRange error if id is set before save

if the id of a record is manually set to an integer rather than letting the database auto-increment the id value (as sometimes happens during import/seeding), then acts_as_having_string_id resets the id to something like "12345678901234567890/12345" and blows up on save with a NumericValueOutOfRange error, seemingly due to it stringifying the wrong thing and creating a very large int for the integer id. here, it thinks 12345 is it's string id, but it's actually the integer id that should have been stringified into something like 1a2B3c4D5e6.

the workaround is to forcibly set the string id using the :id_string class method, but it would be nice if acts_as_having_string_id could detect this situation and automatically handle it appropriately.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.