rails / globalid

Identify app models with a URI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Suggestion] Allow different primary_ids such as UUID

amerritt14 opened this issue · comments

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.

TLDR: I tried to work this out thinking it would be a small and simple change, however the current code expects a specific URL structure (gid:://app/Model/1). Adding this feature will break a lot of things. I wonder if I should move forward with a feature like this at this state.

I stumbled into needing this as well. I was trying to draft something in the source code, however I got a couple thoughts. Having a custom model field for the id like primary_key in your example will demand quite a few changes:

  1. on the Locator's code as everything is done through model's find. We would need to change everything to find_by primary_key: value
    class BaseLocator
  2. URI::GID Expects a specific URL structure, I am not sure how it would break it

I drafted it up something here: https://github.com/rails/globalid/compare/main...rafaelxy:custom-id-in-uri-gid?expand=1

and I got three apparent failures:

Failure:
URI::GIDAppValidationTest#test_apps_containing_non_alphanumeric_characters_are_invalid [/Users/rafaelrichard/src/github.com/rafaelxy/globalid/test/cases/uri_gid_test.rb:122]:
ArgumentError expected but nothing was raised.


rails test test/cases/uri_gid_test.rb:121

................................................F

Failure:
URI::GIDValidationTest#test_too_many_model_ids [/Users/rafaelrichard/src/github.com/rafaelxy/globalid/test/cases/uri_gid_test.rb:92]:
URI::InvalidComponentError expected but nothing was raised.


rails test test/cases/uri_gid_test.rb:91

..F

Failure:
GlobalLocatorTest#test_by_invalid_GID_URI_returns_nil [/Users/rafaelrichard/src/github.com/rafaelxy/globalid/test/cases/global_locator_test.rb:172]:
Expected #<Person:0x00007f8037a2da00 @id="2", @other_id=nil> to be nil.


rails test test/cases/global_locator_test.rb:168

I predict that if I keep going it will break more and more tests on each class I have to meddle since this new model_id_name param will be rippling together with the other URI parts. 🤔

Thanks for taking a look at this, and even working up some code changes!

I have spent a small bit of time working on this as well, feel free to browse my draft PR

With my current implementation (including this gem locally in a project for testing) It still works as it did previously. Not including a primary_key argument just works. Tests, however are failing due to the nature of how Person.find is stubbed to invoke Person.new So there's still some work to make those pass.

The issue I found is that when I introduce a primary_key, it's changing the URI structure, which means the COMPONENT const needs to include the new key conditionally if the primary_key != :id

Basically, If I update the URI expectations to include the primary_key, it breaks backwards compatibility, but if I don't, it doesn't recognize the new URI structure and errors out.

Admittedly, I haven't spent much time working on this, so I'll take a look at your work and see if I'm able to get something more solid put together.

Hey folks, I noticed this issue and wanted to mention that the current version of the gem should support UUID without any changes required. This should happen due to Active Record having a special meaning for the #id method. So as long as an Active Record model has Model.primary_key = :uuid the #id method will return the value of the uuid column along with find() working perfectly fine

The only thing that doesn't work is the locate_many with ignore_missing since the gem hardcoded the id column lookup here

model_class.where(id: ids)

but it should be easily fixable if we change it to where(model.primary_key => ids) which will enforce primary_key to exist on models but that should be fine since Active Record already implements it

  class ModelWithUuidPk
    include ActiveModel::Model
    include GlobalID::Identification

    UUID = "550e8400-e29b-41d4-a716-446655440000"
    def id
      UUID
    end

    def ==(other)
       id == other.id
    end

    def self.find(uuid)
      raise "can't find" if uuid != UUID
      self.new
    end
  end

  test 'uuid primary key' do
    model = ModelWithUuidPk.new
    gid = GlobalID.create(model)
    assert_equal "gid://bcx/GlobalIDParamEncodedTest::ModelWithUuidPk/#{ModelWithUuidPk::UUID}", gid.to_s

    found = GlobalID.find(gid.to_param)
    assert_kind_of ModelWithUuidPk, found

    assert_equal model.to_gid.find, found

    # The only thing that is broken
    GlobalID::Locator.locate_many([ model.to_gid, model.to_gid ])
  end

So I just wanted to share that most of the common use-cases should already be supported. Let me know if that's not exactly the functionality you were looking for. Thanks!

This is fixed now

I ran across this issue and it's not clear to me that the fix actually addresses @amerritt14's original need. I feel like it's addressing @nvasilevski's comment which feels tangentially related

I think I have a similar need to the spirit of @amerritt14's original comment which is that we have a subset of models which have autoincremented primary keys, but also have a UUID column named external_id which we use more publicly.

The hardcoding of model#id in URI::GID.create feels like my blocker.

Given an example user:

User.pluck(:id, :external_id).last
=> [4224, "K0-HUZMT-FT0-S3D"]

Then user.to_gid.to_s will give me "gid://example-app/User/4224"

But if I could provide an argument somewhere to GlobalID to send a different method to model via URI::GID.create, I could get the string I'm hoping for.

gid = URI::GID.build(
  app: "example-app",
  model_name: user.class.name,
  model_id: user.external_id, # this is the important difference
  params: nil
)
gid.to_s
=> "gid://example-app/User/K0-HUZMT-FT0-S3D"

I can live with defining a custom loader. In fact I can see that as likely being desirable.


I'm happy to work on drafting a PR for this feels aligned with maintainers' thoughts on what the library could support. Thoughts @rafaelfranca?