-
Notifications
You must be signed in to change notification settings - Fork 124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow for composite identifiers delimited by /
#163
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this is doing way much than it should. A lot of validations and I don't know what we are trying to protect against.
Do we really need those validations?
lib/global_id/global_id.rb
Outdated
@@ -43,19 +46,22 @@ def parse_encoded_gid(gid, options) | |||
|
|||
def initialize(gid, options = {}) | |||
@uri = gid.is_a?(URI::GID) ? gid : URI::GID.parse(gid) | |||
validate_model_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we need this validation? What are we trying to protect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are trying to prevent someone manually crafting a GID like gid://app/Person/1/2/3/4/5....
which will be parsed as [1,2,3,4,5]
and passed to find()
which may lead to more than one record being loaded.
As an alternative we can move the check to the locator but then custom locators won't be protected. Which may not be a bad thing
Let me know if you'd like to completely loosen the restriction or locator would be a better place to ensure we only target single object when calling find
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GlobalID is primarily used as a serialization format. Someone will not manually be creating GIDs. If people are using GlobalID not a serialization format, then they will be responsible for checking if their input is correct.
I think this is better handled at the locator level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Moved the validation to the locator level. Instead of raising it now returns nil
as this seems to be locator's interface when it comes to locating records by a malformed gid
test/cases/global_id_test.rb
Outdated
assert_nil GlobalID.parse('gid://app/Person/1/2') | ||
assert_nil GlobalID.parse('gid://app/CompositePrimaryKeyModel/tenant-key-value/id-value/something_else') | ||
assert_nil GlobalID.parse('gid://app/CompositePrimaryKeyModel/tenant-key-value/') | ||
assert_nil GlobalID.parse('gid://app/CompositePrimaryKeyModel/tenant-key-value') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we are protecting too much. All those glogal ids should be valid. If they can't be located, isn't a job of the parser to find out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't a job of the parser to find out.
I asked in the comment above but would you prefer Locator
to be the one to check that identifier size matches model's primary key to avoid loading more records than necessary?
63a5ab4
to
81fbe0d
Compare
What do you think about moving Global ID to use The negative point is that Global ID was really built to work with Active Record rather than Active Model (which defines the I guess the class also needed to implement Test script showing current API# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "activemodel"
gem "globalid"
end
require "active_model"
require "global_id"
require "minitest/autorun"
GlobalID.app = "my-app"
class Model
include ActiveModel::Model
attr_accessor :key
include GlobalID::Identification
alias_method :id, :key
@models = {}
def self.create(key:)
@models[key] = new(key:)
end
def self.clear
@models.clear
end
def self.find(key)
if key.is_a?(Array)
key.map { |key| find(key) }
else
@models.fetch(key)
end
end
def self.where(id:)
if id.is_a?(Array)
id.flat_map { |key| where(id: key) }
else
Array(@models[id])
end
end
end
class ModelGIDTest < Minitest::Test
def test_to_global_id
assert_equal "gid://my-app/Model/foo", Model.new(key: "foo").to_global_id.to_s
end
def test_locate
assert_raises { GlobalID::Locator.locate("gid://my-app/Model/foo") }
model = Model.create(key: "foo")
assert_equal model, GlobalID::Locator.locate("gid://my-app/Model/foo")
ensure
Model.clear
end
def test_locate_many
assert_raises do
GlobalID::Locator.locate_many(["gid://my-app/Model/foo", "gid://my-app/Model/bar"])
end
foo = Model.create(key: "foo")
assert_raises do
GlobalID::Locator.locate_many(["gid://my-app/Model/foo", "gid://my-app/Model/bar"])
end
assert_equal [foo], GlobalID::Locator.locate_many(["gid://my-app/Model/foo", "gid://my-app/Model/bar"], ignore_missing: true)
bar = Model.create(key: "bar")
assert_equal [foo, bar], GlobalID::Locator.locate_many(["gid://my-app/Model/foo", "gid://my-app/Model/bar"])
ensure
Model.clear
end
end Diff for the test script to support this branch's APIdiff --git i/test_gid.rb w/test_gid.rb
index 91730c79cc..0db53a1f42 100644
--- i/test_gid.rb
+++ w/test_gid.rb
@@ -6,7 +6,7 @@
source "https://rubygems.org"
gem "activemodel"
- gem "globalid"
+ gem "globalid", github: "https://github.com/rails/globalid/pull/163"
end
require "active_model"
@@ -39,13 +39,17 @@ def self.find(key)
end
end
- def self.where(id:)
- if id.is_a?(Array)
- id.flat_map { |key| where(id: key) }
+ def self.where(key:)
+ if key.is_a?(Array)
+ key.flat_map { |key| where(key: key) }
else
- Array(@models[id])
+ Array(@models[key])
end
end
+
+ def self.primary_key
+ :key
+ end
end
class ModelGIDTest < Minitest::Test |
5cf6818
to
7acd2a4
Compare
Hey @etiennebarrie , great question! IIRC @nvasilevski and I discussed using That said, it's somewhat of a moot point given that, as you mentioned, Active Records inherently define EDIT: Okay, so a blocker to using globalid/lib/global_id/locator.rb Lines 168 to 173 in 7acd2a4
Since we don't have an instance here, we can't use Probably best to stick with |
This commit extends global id to allow representing models with composite identifiers. The value will be joined by `/`. For example: Given a `TravelRoute` model with `origin` and `destination` as the compsoite primary key, the global id will be represented as: ``` TravelRoute.new(origin: "Ottawa", destination: "New York").to_global_id => gid://app/TravelRoute/Ottawa/New%20York ``` Co-authored-by: Adrianna Chang <adrianna.chang@shopify.com> Co-authored-by: Nikita Vasilevsky <nikita.vasilevsky@shopify.com>
7acd2a4
to
5ce154c
Compare
cc @byroot @rafaelfranca could I get y'all to take another look at this? Thanks! |
else | ||
model_class.find(ids) | ||
end | ||
end | ||
|
||
private |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private |
primary_key = Array(gid.model_class.primary_key) | ||
primary_key_size = primary_key.size | ||
|
||
Array(gid.model_id).size == primary_key_size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
primary_key = Array(gid.model_class.primary_key) | |
primary_key_size = primary_key.size | |
Array(gid.model_id).size == primary_key_size | |
Array(gid.model_id).size == Array(gid.model_class.primary_key).size |
ids_by_model.each do |model, ids| | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ids_by_model.each do |model, ids| | |
ids_by_model.each do |model, ids| |
hmm, those are small thins I can fix myself. Merging. |
FYI #168 |
Broken CI is due to rails/globalid#163 in globalid 1.2.0. https://buildkite.com/rails/rails/builds/99329#018a5f01-a966-4424-9596-0a7f1deeb1ff/1178-1190
I won't debate its fairness, but that expectation isn't conveyed well. The README says "Mix GlobalID::Identification into any model with a #find(id) class method. Support is automatically included in Active Record." We didn't interpret that to mean that we needed to define
That's probably true, but please don't forget about the few models that aren't coming from ActiveRecord. Documentation about the API that is required and treating that required API spec as part of the public API for globalid would be very much appreciated. |
This commit extends global id to allow representing models with composite identifiers. The value will be joined by
/
. For example:Given a
TravelRoute
model withorigin
anddestination
as the compsoite primary key, the global id will be represented as:Context
Next version of Rails will support models with a composite primary key and globalid need to provide capabilities to present such models. One of the major use-cases is the Active Record objects serialization in Active Job jobs
Major changes
primary_key
class methodChoosing the delimiter
We end up choosing
/
as a delimiter since it has the least change of conflicting with one of the values of the composite primary key. However it leads to slightly changed semantic when it comes to splitting the segments of the gidFor example a broken URI like
'gid://app/alsoapp/Person/12'
used to considered invalid due to malformed app name. But now the same URI will be parsed as:Basically this is the only concern with the
/
being used as a delimiterAs an alternative we were considering
_
to serve as a delimiter but since the delimiter value is going to be hardcoded we decided against it as it has a higher chance of conflicting with the actual value of the composite keyWhat reviewers should be focusing on
a global id like
'gid://app/Person/1/2'
used to be considered invalid. After the change it will be possible to parse and initialize anURI::GID
instance and1/2
will be parsed as['1','2']
composite key however it won't be possible to look up records usingGlobalID
instance created from suchURI::GID
To prevent malicious actors from crafting gids with unrealistically long composite keys we have limited the maximum size of the composite key by
20
This has been done to prevent issues like GHSA-23c2-gwp5-pxw9
Integration test
Here is a script that tests changes in integration with Active Job
Expand