rails / globalid Goto Github PK
View Code? Open in Web Editor NEWIdentify app models with a URI
License: MIT License
Identify app models with a URI
License: MIT License
Currently GlobalID assumes that the GID model name exists as a constant defined in the top-level namespace. This is fine as a default, but I should be able to lookup models however I want if I'm using a customer locator.
Real world use case:
I have a monolithic application that I plan to break up into smaller services in the future. We are experimenting with GIDs to facilitate that transition.
Most GIDs look like 'gid://my-app/Person/1'
.
For resources in namespaced modules that will eventually be refactored out, GIDs look like 'gid://my-module/Customer/1'
. I have defined a custom locator for each module to handle this:
GlobalID::Locator.use :'my-module', MyModuleLocator.new
class MyModuleLocator
def locate(gid)
MyModule.const_get(gid.model_name).find(gid.model_id)
end
end
However, this does not work—GlobalID.find('gid://my-module/Customer/1')
raises a LoadError.
This is because GlobalID::Locator::find
invokes GlobalID#model_class
(which tries to Object.cont_get()
the model name) before my custom locator is ever invoked [1]. Of course this raises a LoadError, because 'Customer'
lives in the MyModule
namespace.
[1] https://github.com/rails/globalid/blob/v0.3.6/lib/global_id/locator.rb#L17
The documentation for Signed Global IDs shows that they can have an expiry date. I know that if the SGID is expired, then trying to use it to locate a record returns nil
. But returning nil
could also mean that the record indicated by the SGID no longer exists.
Is there a way to specifically tell that the SGID is expired? I see in the code, an ExpiredMessage
exception is actually raised, but it is immediately caught and turned into nil
.
globalid/lib/global_id/signed_global_id.rb
Lines 32 to 40 in 3ddb0f8
I could use the SignedGlobalID#verifier
to decode the message and then get the expired_at
attribute out of the Hash. That seems hacky to me, so I was wondering if there was another way to check for expiration?
Would a PR to add an expired?
method to SignedGlobalID
be welcome? I'd be happy to work on that.
Thanks!
Hi,
I am trying to use the purpose as specified in the readme file.
Readme file has the folowing example given:
signup_person_sgid = Person.find(1).to_sgid(for: 'signup_form')
=> #<SignedGlobalID:0x007fea1984b520
I am trying this out in the rails 4.2.0 console. My command line is:
siddharth@siddharth-Inspiron-5537:~/Development/ROR/ror_2/app1$ rails console
Loading development environment (Rails 4.2.0)
irb(main):001:0> user = User.find(1).to_sgid
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
=> #<SignedGlobalID:0x007f4ba64aa2b0 @uri=#<URI::GID gid://app1/User/1>, @Verifier=#<ActiveSupport::MessageVerifier:0x007f4ba5448110 @secret="\xFD7\v<\xCF\x17;Q\x0E=\x8FRzv\xD6\xE7\xAB\x98Gp\xDC\xE9\xD1\r\xC2\x87\xD1\x94 \xD5\xCDN\x04\xEB\xFDBIA\x1F\x19W\xAA\xBC\x85\xD3S>\xEF\xAA\x91\xEC\r\xF2\x0F\x1Dv\x86\xCF\xEB\xED\n\x0EJ\xC1", @digest="SHA1", @Serializer=Marshal>, @purpose="default", @expires_at=Thu, 09 Apr 2015 20:04:00 UTC +00:00>
irb(main):002:0> user = User.find(1).to_sgid(for: 'signup_form')
irb(main):003:1>
User.find(1).to_sgid works well. However, it seems using for: as a parameter is creating a loop.
Also expire and all other things are working fine. I only have problem with the purpose implementation here.
Maybe I am doing this incorrectly. Can someone guide me through this?
We use a multi-database based multi-tenant model in our app. This is not supported by the current gid format. A job scheduler has no way of finding out if "gid://MultiTenantApp/User/1" references the user in the database "tenant_1" or "tenant_2".
Some way to include the database name or tenant id would be very useful. Alternatively some way to easily add custom fields to gid would certainly be helpful.
Sequel doesn’t follow the same protocol as ActiveRecord, even when ActiveModel compliant (or so it seems). I’ve come up with what looks like the right level of monkey-patching for this, and can turn this into a real patch (with tests) for GlobalID if there is interest. The changes are only necessary in GlobalID::Locator::BaseLocator
for #locate
and #find_records
.
The only other change is including GlobalID::Identification into Sequel::Model, but in my basic testing, these changes create the same result as AR-backed GlobalID, mod the appropriate exceptions.
Also opened as TalentBox/sequel-rails#111.
module SequelBaseLocator
def locate(gid)
if defined?(::Sequel::Model) && gid.model_class < Sequel::Model
gid.model_class.with_pk!(gid.model_id)
else
super
end
end
private
def find_records(model_class, ids, options)
if defined?(::Sequel::Model) && model_class < Sequel::Model
model_class.where(model_class.primary_key => ids).tap do |result|
if !options[:ignore_missing] && result.count < ids.size
fail Sequel::NoMatchingRow
end
end.all
else
super
end
end
end
GlobalID::Locator::BaseLocator.prepend SequelBaseLocator
Sequel::Model.send(:include, ::GlobalID::Identification)
12f7629 removed the ability to keep using the old format when serializing via SignedGlobalID
. This causes the following issues:
I understand that "Rollbacks" can be considered a "no-issue" since the upgrade is one-way but I'm still afraid of the rolling-deployment issue. Additionally, Version "1.2.1" is the new version pointed by Rails "7.1", which makes rolling back a Rails upgrade more complicated because it forces you to upgrade GlobalID before upgrading Rails in order to not encounter the "unexpected" rollback issue.
#!/usr/bin/env ruby
require "bundler/inline"
globalid_version = ARGV[0]
$sgid = ARGV[1]
# true = install gems so this is fast on repeat invocations
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "globalid", globalid_version
gem "activerecord", "~> 7.1"
gem "sqlite3"
end
require "active_record"
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table "people" do |t|
t.string "name"
end
end
class Person < ActiveRecord::Base; end
SignedGlobalID.verifier = ActiveSupport::MessageVerifier.new("secret")
person = Person.create!(name: "John Doe")
if $sgid
require "minitest/autorun"
class SignedGlobalIdTest < Minitest::Test
def test_cant_locate_new_format_sgid_with_old_version
assert GlobalID::Locator.locate_signed($sgid), "Can't locate by SGID"
end
end
else
puts SignedGlobalID.create(person, app: "test")
end
jacopo-37s-mb 3.3.0-preview2 ~ ./signed_globalid_serializarion_issue.rb 1.2.0 | tail -n 1 | xargs ./signed_globalid_serializarion_issue.rb 1.2.0
-- create_table("people")
-> 0.0081s
Run options: --seed 25563
# Running:
.
Finished in 0.014660s, 68.2128 runs/s, 68.2128 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
jacopo-37s-mb 3.3.0-preview2 ~ ./signed_globalid_serializarion_issue.rb 1.2.0 | tail -n 1 | xargs ./signed_globalid_serializarion_issue.rb 1.1.0
-- create_table("people")
-> 0.0093s
Run options: --seed 3475
# Running:
F
Finished in 0.000923s, 1083.4238 runs/s, 1083.4238 assertions/s.
1) Failure:
SignedGlobalIdTest#test_cant_locate_new_format_sgid_with_old_version [./signed_globalid_serializarion_issue.rb:36]:
Can't locate by SGID
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
jacopo-37s-mb 3.3.0-preview2 ~
Thanks for maintaining globalid.
I am trying to making rails 5.2 available in Debian. During our tests we found this gem fail on tests with rails 5.2:
Failure:
RailtieTest#test_SignedGlobalID.verifier_defaults_to_nil_when_secret_token_is_not_present [/home/travis/build/alee-ccu/globalid/test/cases/railtie_test.rb:41]:
Expected #<GlobalID::Verifier:0x00007f7558521b40 @secret="\xE9\xCD\xAB\xCE\xFF\x93v\x14\xF1\a\xE7/\e\xB2<\xE0\xE3\xA5~\r\xC7\xB5\xEB\xC9`PFx\f\x13\x04vs\x95\xDB\xB1`U\x8FM^(\x7FQ\x7F\xED\xE7U;\xF8r\xB7\x87\xF6\xC8;o\xE7\xBB\xCC\x8E\xFB\xE6\x94", @digest="SHA1", @serializer=Marshal, @options={}, @rotations=[]> to be nil.
I also reproduced the failed tests on travis-ci.org
with both version 0.4.1
and master
branch, please see the full log at:
https://travis-ci.org/alee-ccu/globalid/jobs/464195985
https://travis-ci.org/alee-ccu/globalid/jobs/464202126
We may want to suppress the warnings for deprecated Fixnum/Bignum on Ruby 2.4.0.
$ ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]
$ bundle -v
Bundler version 1.14.3
$ rm Gemfile.lock (<= remove the `Gemfile.lock` file because tests on Ruby 2.4.0 are failed for activesupport-4 right now)
$ bundle install --path vendor/bundle
$ bundle list
Gems included by the bundle:
* actionpack (5.0.1)
* actionview (5.0.1)
* activemodel (5.0.1)
* activesupport (5.0.1)
* builder (3.2.3)
* bundler (1.14.3)
* concurrent-ruby (1.0.4)
* erubis (2.7.0)
* globalid (0.3.7)
* i18n (0.8.0)
* loofah (2.0.3)
* method_source (0.8.2)
* mini_portile2 (2.1.0)
* minitest (5.10.1)
* nokogiri (1.7.0.1)
* rack (2.0.1)
* rack-test (0.6.3)
* rails-dom-testing (2.0.2)
* rails-html-sanitizer (1.0.3)
* railties (5.0.1)
* rake (12.0.0)
* thor (0.19.4)
* thread_safe (0.3.5)
* tzinfo (1.2.2)
$ bundle exec rake test 2> test_err.log
...
127 runs, 229 assertions, 0 failures, 0 errors, 0 skips
$ grep deprecated test_err.log | grep -v 'vendor/bundle' | sort | uniq
/home/jaruga/git/globalid/test/cases/global_id_test.rb:105: warning: constant ::Fixnum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:106: warning: constant ::Fixnum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:115: warning: constant ::Bignum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:96: warning: constant ::Fixnum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:97: warning: constant ::Fixnum is deprecated
Fixnum
to Integer
, Bignum
to Integer
.Fixnum
to 0.class
like rails
case, rails/rails@cb0452eExplicitly setting config.global_id.expires_in = nil
does not result in non-expiring SGIDs and instead results in the app assuming the default SGID lifetime.
Is there a way for doing something similar to what is described bellow?
user = User.create(params)
uid = user.to_sgid(access_count: 1).to_s
GlobalID::Locator.locate_signed(uid) # => returns user
GlobalID::Locator.locate_signed(uid) # => returns nil
It'd be nice for the readme to say a bit about to_param
and GlobalID::Locator.locate
working together, as it enables this:
view: link_to x, somepath(content_gid: content.to_global_id)
controller: content = GlobalID::Locator.locate params.fetch(:content_gid)
It looks like some thought has gone into this, but it's not currently advertised as a feature.
Would a readme PR be welcome for this or is it an internals thing we shouldn't be relying on?
I noticed a recent change now requires any class that has include GlobalID::Identification
must now also respond to .primary_key
. I think it came from #163
I'm not sure if GlobalId is intended to be used on only Active Record / Active Model, but that's what I've been doing. The readme says:
Mix GlobalID::Identification into any model with a
#find(id)
class method
If model in this sense means generically "a domain model" than I think this is a bug or breaking change. And if not, the readme needs to be updated to say both .find
and .primary_key
.
The initial URI implementation expects the Railtie to set GlobalID.app = 'name'
.
If that's nil
, nothing complains currently. GlobalID.create
should raise an optional :app
keyword argument that defaults to GlobalID.app
and raise ArgumentError
if app
is nil
.
Model.new.to_global_id
currently raises a URI::InvalidComponentError
with the message URI::InvalidComponentError: Expected a URI like gid://app/Person/1234: #<URI::GID gid://app>
. An error particular to GlobalID
would be useful:
GlobalID::UnidentifiableRecord Unable to identify #{record.class} without an id. (Maybe you forgot to call save?)
This is already being discussed in rails/rails#19861 and rails/rails#19877.
Would like to suggest to add class method like GlobalID.create
but where you can pass model_class
and model_id
directly.
This is helpful when you need to create GlobalID without object, for example in polymorphic associations where you have two columns with class
and id
(like owner_type and owner_id)
for example
GlobalID.create('User', 4, options)
alternatively currently we have to do
User.new(id: 4).to_global_id
which results in unnecessary object creation
Signing a message prevents it from being tampered with, but it doesn't prevent it from being reused in another context.
For example, an app that accepts a User#sgid
as a remember-me token and uses User#sgid
for a list of forum members.
Someone can take the signed, tamperproof Global ID from the forum member list and stick it in their remember-me token and, voila, they're signed in as that user.
To prevent this class of reuse and replay, signed messages need to indicate their purpose and include it in the signed content. The purpose of the signed Global ID also needs to be tamperproof.
login_sgid = Person.find(1).signed_global_id(for: 'login')
GlobalID::Locator.locate_signed(login_sgid, for: 'like_button')
# => BOOM!
GlobalID::Locator.locate_signed(login_sgid, for: 'login')
# => Person id=1
From documentation ...
In Rails, an auto-expiry of 1 month is set by default. You can alter that deal in an initializer with:
# config/initializers/global_id.rb
Rails.application.config.global_id.expires_in = 3.months
Unfortunately in my case it loads after railtie initializer (https://github.com/rails/globalid/blob/master/lib/global_id/railtie.rb#L16)
As result, it does not work, because SignedGlobalID.expires_in is set with default 1.month
My application details:
Rails 5.1.2
*** LOCAL GEMS ***
globalid (0.4.0)
As a workaround, I put config.global_id.expires_in = 3.months
to config/application.rb
Should be fixed or the doc should be updated
When i have set a default_scope to my model, GlobalID can't find it. https://github.com/rails/globalid/blob/master/lib/global_id/locator.rb#L130
Error while trying to deserialize arguments: Couldn't find Image with 'id'=75
Maybe it should always be set to unscoped
?
I have an external database which has text identifiers, some of which end in a trailing space.
When using global ids with these records, the global ids generated fail to be parsed, and therefore lookup also fails.
Perhaps model ids should be urlencoded?
I have a similar concept that I use in some apps when syncing data. I call it a source_token
The format is almost identical, except that it uses UUID instead of ID. Would there be interest in allowing an optional parameter which would serialize the uuid
instead?
I'm imagining something along these lines:
User.find_by(uuid: "abc-123-uuid").to_global_id(primary_key: :uuid).to_s
=> "gid://myapp/User/uuid/abc-123-uuid"
The presence of a value between the class
and id
would indicate a different method of lookup. In order to maintain backwards compatibility, it would assume id
if no other value is specified.
I'm happy to take a stab at implementing but I would love any insight around the acceptance of such a feature.
Upgrading an internal app to Rails 4.2 beta, and this alias hit me pretty hard. The app deals with file system permissions and as gid is the numeric group id, it is used heavily in modeling file system users.. Since now all my AR models can't use gid as an field anymore. so lots of renaming columns. Tried aliasing it, but that didn't work (as expected) Not sure if I should post this here or Active Record? Figure since this is where it is defined. I am probably not the only person to be bit by this. No real work around, that I can see..
Once we can sign with purpose, we'll also want to be explicit about how long the signed Global ID is valid. It needs an expiration date!
See the work in progress on expiration @ rails/rails#16462 - they can use some help on this as well ❤️
We'll want to be able to pass :expires_in
or :expires_at
when we create signed Global IDs. When we parse a sgid, we'll rely on the MessageVerifier to raise when it's past the expiration date. We'll have to rescue that error and return nil
.
Furthermore, we'll want expiration by default, so we'll never inadvertently send out forever-valid signed Global IDs. So, SignedGlobalID.expires_in = 1.month
for example, and expose config.global_id.expires_in = ...
to the Railtie. Allow passing expires_in: nil
to override and use no expiry.
My app switches to a different database based on subdomain using:
Mongoid.override_database("timeline_#{subdomain}_production")
So when the globalid is generated, it looks something like:
gid://timeline/User/55230a4278616e6fbc010000
This does not help because this ID is meaningless without knowing what the subdomain or the database name is. I am looking for something like:
gid://timeline/database_name/User/55230a4278616e6fbc010000
So that ActiveJob or whatever is responsible for finding the record knows where to actually look for it.
Any ideas/suggestions?
P.S. this is related to this issue: rails/activejob#112
The params
method returns nil. I haven't look much further into it yet.
This way signed global IDs can be compared even if generated in different places (we get this issue with html select elements, and with ids generated across the app).
Example code to add to ApplicationController
:
before_action :set_sgid_expiry
# Set a default expiry for SGIDs which means that any ids generated within this request will have the same value
# and can thus be matched on the frontend
def set_sgid_expiry
SignedGlobalID.expires_at 1.week.from_now
end
Would this be something viable?
"With Global IDs, we have a universal identifier that works for objects both classes."
Unsure the implied meaning (objects of both classes?), so haven't updated it myself.
If no verifier is given in the configuration we should't provide a default one in the railtie because rails should be able to initialize without secrets.yml but the rails message_verifier raises an exception if it doesn't get the secret_key_base from secrets.yml. Currently adding globalid as a dependency to rails breaks the rails tests (https://travis-ci.org/rails/rails/jobs/32713713#L872)
So locating a gid doesn't mean looking up arbitrary objects.
With a class:
GlobalID::Locator.locate gid, only: [ User, Group ]
With a module:
GlobalID::Locator.locate gid, only: Subscribable
If the gid references a class that doesn't have only.any? { |mod| klass < mod }
, return nil
.
We just caught the following during code review:
model.to_sgid(purpose: :api, expires_at: 1.day.from_now.end_of_day)
The correct option to pass is for
instead of purpose
. This went unnoticed because everything still works, but the default
purpose is used.
Maybe it would make sense to raise an error in case an unknown option is passed to to_sgid
(and friends)?
locate_many
does not work properly when different objects are passed.
GlobalID::Locator.locate_many ['gid://app/User/3', 'gid://app/Custom::Item/74592']
The issue User
is active_record model and responds to :unscoped, Custom::Item
is custom model and does not support :unscoped
method.
As a result the above code returns
NoMethodError: undefined method 'unscoped' for Custom::Item:Class
but works if you swap the order of objects.
The initial URI implementation leans on Ruby's URI::Generic and acts as a URL, using the hostname as the app namespace and the first two path segments as model class and id.
Let's formalize this with an actual Global ID URI scheme that takes advantage of Ruby's parser and validation.
Global IDs can be URI-escaped and embedded in an app's routes and query params, but the percent escaping and needless detail makes a mess of the URL.
Let's provide a nice compact #to_param
representation for these cases:
Person.find(1).gid.to_param
# => "Z2lkOi8vYXBwL1BlcnNvbi8x"
(That's just def to_param; Base64.strict_encode64 to_s end
)
The locator can try to Base64-decode non-URI arguments:
GlobalID::Locator.locate "Z2lkOi8vYXBwL1BlcnNvbi8x"
# => Person id=1
Looks like the syntax used here: https://github.com/rails/globalid/blob/master/lib/global_id/locator.rb#L126 for ignore_missing
is not supported in earlier versions of ruby.
In my current project I use a lot of DI in the form of:
def do_something(first: SomeClass, second: SomeClass.method(:some_method))
# ...
end
Passing my dependencies to an ActiveJob fails with an ActiveJob::SerializationError: Unsupported argument type: Class
(or respectively Unsupported argument type: Method
). However, I think this could be easily done by serialising methods and classes into:
SomeClass.method(:some_method).to_global_id # => gid://App/SomeClass/some_method
SomeClass.to_global_id # => gid://App/SomeClass/class
Anyone interested in this feature? Suggestions?
Let apps set the gid app portion of the URL:
config.global_id.app = 'myapp'
This defaults to Rails.application.railtie_name.remove('_application')
currently, so a MyBlog::Application
app gets gids like gid://MyBlog/Post/1234
Let apps provide a signed message verifier:
config.global_id.verifier = custom_verifier
Some of you might be familiar with the "No Way, JOSE! Javascript Object Signing and Encryption is a Bad Standard That Everyone Should Avoid" post.
Recently at work we began sending a callback URL to a 3rd party. To verify that it's indeed the 3rd party that's calling back the endpoint, someone suggested putting a JSON web token in the payload. I suggested using signed Global IDs, since they're accessible OOTB in Rails and also, in my impression, are somewhat more secure than JWT.
However, in the context of that post, I was thinking if there are ways in which signed Global IDs might be misused. I'm not a security expert, so it's hard for me to grasp if this library can be used in unsecure ways.
I suppose it wouldn't be that great for storing sessions for the same reasons as JWT. If there's a need to invalidate certain signed IDs, globalid doesn't handle this out of the box, so that also might not be that great of a pick.
Anything else that comes to your mind?
Hello. I would like to ask you if there is an specific reason why not to optionally allow eager load of relationships on locate/locate_many.
I mean something like this:
def locate_many(gids, options = {})
models_and_ids = gids.collect { |gid| [ gid.model_class, gid.model_id ] }
ids_by_model = models_and_ids.group_by(&:first)
loaded_by_model = Hash[ids_by_model.map do |model, ids|
[ model, find_records(model, ids.map(&:last), includes: options[:includes], ignore_missing: options[:ignore_missing]).index_by { |record| record.id.to_s } ]
end]
models_and_ids.collect { |(model, id)| loaded_by_model[model][id] }.compact
end
private
def find_records(model_class, ids, options)
if options[:ignore_missing]
model_class.includes(options[:includes]).where(id: ids)
else
model_class.includes(options[:includes]).find(ids)
end
end
def
It would be not exactly like that code, but well, it's an idea.
Thanks in advance
RailtieTest#test_GlobalID.app_can_be_set_with_config.global_id.app_= = 0.43 s = E
Error:
RailtieTest#test_GlobalID.app_can_be_set_with_config.global_id.app_=:
Errno::ENOENT: No such file or directory @ rb_sysopen - /<<PKGBUILDDIR>>/tmp/development_secret.txt
test/cases/railtie_test.rb:27:in `block in <class:RailtieTest>'
bin/rails test test/cases/railtie_test.rb:25
RailtieTest#test_GlobalID.app_for_Blog::Application_defaults_to_blog = 0.40 s = E
Error:
RailtieTest#test_GlobalID.app_for_Blog::Application_defaults_to_blog:
Errno::ENOENT: No such file or directory @ rb_sysopen - /<<PKGBUILDDIR>>/tmp/development_secret.txt
test/cases/railtie_test.rb:21:in `block in <class:RailtieTest>'
bin/rails test test/cases/railtie_test.rb:20
RailtieTest#test_config.global_id_can_be_used_to_set_configurations_after_the_railtie_has_been_loaded = 0.44 s = E
Error:
RailtieTest#test_config.global_id_can_be_used_to_set_configurations_after_the_railtie_has_been_loaded:
Errno::ENOENT: No such file or directory @ rb_sysopen - /<<PKGBUILDDIR>>/tmp/development_secret.txt
test/cases/railtie_test.rb:38:in `block in <class:RailtieTest>'
bin/rails test test/cases/railtie_test.rb:31
Finished in 1.367685s, 97.2446 runs/s, 174.7479 assertions/s.
133 runs, 239 assertions, 0 failures, 3 errors, 0 skips
I think there's no directory, perhaps we could add the secret token?
tok = User.first.to_sgid(expired_in: Time.current)
...some time later...
GlobalID::Locator.locate_signed tok.to_s =>
SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> #<User:0x00007f83d626bae0
id: 1,
...
When multiple apps collaborate and reference each others' Global IDs, they'll use different means to locate the URI references.
For example, locate an Active Resource class:
GlobalID::Locator.use :foo do |gid|
FooRemote.const_get(gid.model_name).find(gid.model_id)
end
GlobalID::Locator.locate 'gid://foo/Account/1234'
# => FooRemote::Account id=1234
Or locate the referenced model in a search index:
GlobalID::Locator.use :bar, BarLocator.new
class BarLocator
def locate(gid)
@search_client.search name: gid.model_name, id: gid.model_id
end
end
GlobalID::Locator.locate 'gid://bar/Account/1234'
# => BarResult name=Account id=1234
Hello globalid,
Could you please also add some metadata information of this gem in this page https://rubygems.org/gems/globalid/edit 🙏:
Here are some I know:
URL | VALUE |
---|---|
Source Code URL | https://github.com/rails/globalid |
Documentation URL | http://www.rubydoc.info/gems/globalid |
Bug Tracker URL | https://github.com/rails/globalid/issues |
Thank you!
Sometimes might want to explicitly declare which class to use for a given class, instead of letting GID infer it.
For example:
class Post
def self.gid_class
Post.name
end
end
class Post::Foo < Post
end
class Post::Bar < Post
end
and
module URI
class GID < Generic
class << self
def create(app, model, params = nil)
model_name = model.try(:gid_class) || model.class.name
build app: app, model_name: model_name, model_id: model.id, params: params
end
end
end
end
The main usage case is GID + ActiveJob. For example, if update a type
of record, all old Jobs will be failed, because they will not be able to find a record by old URL.
So, any ideas how to resolve it, or better to implement something like this?
PS: Solution by https://github.com/elabs/pundit/tree/v1.0.1#manually-specifying-policy-classes
So If you give the locator "http://google.com": it will reach extract_uri_components
which will raise URI::BadURIError, "Not a gid:// URI scheme: #{@uri.inspect}" unless @uri.scheme == 'gid'
but #parse
rescues that (because URI::BadURIError
is a subclass of URI::Error
) and tries to base64 decode.
We should rescue URI::BadURIError
in #parse
and return nil
From @jwoertink on April 19, 2017 19:16
I don't get this error often, but sometimes I get
Error while trying to deserialize arguments: uninitialized constant GlobalID::Locator Did you mean? GlobalID::Locator
I'm not able to recreate this error locally, and out of 310 processed jobs, it's only shown up twice.
I'm using Sidekiq with ActiveJob on Rails 5.0.2 with ruby 2.4.1.
Here's the data from Sidekiq
Job
Queue default
Job {"job_class"=>"EncoderJob", "job_id"=>"2c6589f6-172e-4e83-9910-c4ffc29bb5d0", "queue_name"=>"default", "priority"=>nil, "arguments"=>["update", "poster", 54381, ["hq_gallery_zip_url", "promo_photo_pack_zip_url", "updated_at"]], "locale"=>"en"}
Arguments
JID 6e64e144c2eea7c09d418b53
Enqueued 2 days ago
Extras {"wrapped"=>"EncoderJob", "processor"=>"d14dcdc7631e:1"}
Error
Error Class ActiveJob::DeserializationError
Error Message Error while trying to deserialize arguments: uninitialized constant GlobalID::Locator Did you mean? GlobalID::Locator
Copied from original issue: rails/rails#28801
The issue as it seems global id cached on the model and if it is called more than once with different params it returns wrong result.
Check this out
> User.find(1).to_sgid.to_s == User.find(1).to_sgid(for:'asd').to_s
User Load (1.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> false
And compare to this:
> u = User.find(1)
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> #<User:0x007febc9f74f38 ...>
> u.to_sgid.to_s == u.to_sgid(for:'asd').to_s
=> true
The code I am providing here is to illustrate an idea, a seed for discussion, not a proposal to adopt it as-is.
Nevertheless, I have put together some handy code that lets me locate resources that are embedded within other resources, for example, embedded within a serialized column, like a postgres jsonb structure. I could make another gem out of this, but some form of this idea seems pretty generally useful. I wonder if the community here has any thoughts on whether this idea is worth iterating on, or importing into the globalid project.
The concept is a regular URI::GID where a "host model" can be located in the normal way, but adds additional params
to the URI::GID
in order to locate an object that is only locatable within its host. This implementation imposes no requirements on the name or format of those params, only that whatever params exist can be used by the "host model" to locate the required sub-resource. The host model need only implement an instance method, gid_sublocate
in this case, and given the gid property, it can do whatever work is needed to find the specified subresource.
In a nutshell, this means resources that are not normally globally addressable can become globally addressable by leaning on the addressability of some host model.
# Allows objects to be located within other objects and referenced through the
# GlobalID gem. In order to participate, the sublocatable's class must
# implement an instance method `to_global_id` and the host class must implement
# `gid_sublocate`
#
# # Returns a sublocated object if one can be found, for example:
# def gid_sublocator(gid)
# key = gid.params[:some_key]
# serialized_column[key]
# end
#
# # Returns a GlobalID object which will be passed to gid_sublocator to
# # locate this object later, for example:
# def to_global_id(options = {})
# GlobalID.new(
# URI::GID.build(
# app: GlobalID.app,
# model_name: 'Host::Model',
# model_id: 123,
# params: {
# some_key: 'abc'
# }
# )
# )
# end
class GidSublocator < GlobalID::Locator::UnscopedLocator
def locate(gid, options = {})
sublocate(super, gid)
end
def locate_many(gids, options = {})
super.zip(gids).map do |(located, gid)|
sublocate(located, gid)
end
end
private def sublocate(located, gid)
located.try(:gid_sublocate, gid) || located
end
end
Just trying Global ID with an app called PracticeManager -- this becomes practice_manager
when creating a global ID URI, which causes URI::InvalidURIError
, I believe due to the underscore:
URI::InvalidURIError: the scheme gid does not accept registry part: practice_manager (or bad hostname?)
from /Users/styrmis/.rvm/rubies/ruby-2.0.0-p0/lib/ruby/2.0.0/uri/generic.rb:214:in `initialize'
Setting config.global_id.app
to a name which doesn't contain an underscore causes this error to go away.
Hello. There was a great feature added - hash equality. Can somebody release it, please?
I have an ActiveJob created in the after_save
callback. The job gets the instance as a parameter. Every now and then when the job executes the instance is stale - it contains the data from before the model update. I have observer in the logs that the instance is not being read from the database - it must be coming from cache. Debugging the updated_at
column showed that the value in the Job is earlier than the the one saved in the database.
after_save do |instance|
SaveAttachmentsJob.perform_later(instance)
end
class SaveAttachmentsJob < ActiveJob::Base
queue_as :default
def perform(instance)
# This fixes the issue.
# instance.reload
# Every now and then this will show stale object.
Rails.logger.info(instance.inspect)
# ... some more code.
end
end
I have worked around the problem calling instance.reload
in the Job.
globalid 0.3.5, active job 4.2.3, I am using Sidekiq to process the Jobs.
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.