Skip to content

Commit

Permalink
Maintenance: Add GraphQL helpers to convert Global IDs to Internal ID…
Browse files Browse the repository at this point in the history
…s without SQL query
  • Loading branch information
mantas committed Dec 4, 2024
1 parent de6a46a commit 73e55ee
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 11 deletions.
42 changes: 39 additions & 3 deletions app/graphql/gql/zammad_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ def self.resolve_type(abstract_type, obj, _ctx)

# Relay-style Object Identification:

# Return a string UUID for the internal ID.
# Return a string GUID for the internal ID.
def self.id_from_internal_id(klass, internal_id)
GlobalID.new(::URI::GID.build(app: GlobalID.app, model_name: klass.to_s, model_id: internal_id)).to_s
end

# Return a string UUID for `object`
# Return a string GUID for `object`
def self.id_from_object(object, _type_definition = nil, _query_ctx = nil)
object.to_global_id.to_s
end

# Given a string UUID, find the object.
# Given a string GUID, find the object.
def self.object_from_id(id, _query_ctx = nil, type: ActiveRecord::Base)
GlobalID.find(id, only: type)
end
Expand All @@ -72,6 +72,42 @@ def self.authorized_object_from_id(id, type:, user:, query: :show?)
end
end

# Given a string GUID, extract the internal ID.
# This is very helpful if GUIDs have to be converted en-masse and then authorized in bulk using a scope.
# Meanwhile using .object_from_id family would load (and, optionally, authorize) objects one by one.
# Beware there's no built-in way to authorize given IDs in this method!
#
# @param id [String] GUID
# @param type [Class] optionally filter to specific class only
#
# @return [Integer, nil]
def self.internal_id_from_id(id, type: ActiveRecord::Base)
internal_ids_from_ids([id], type:).first
end

# Given an array of string GUIDs, extract the internal IDs
# @see .internal_id_from_id
#
# @param ids [Array<String>] GUIDs
# @param type [Class] optionally filter to specific class only
#
# @return [Array<Integer>]
def self.internal_ids_from_ids(...)
local_uris_from_ids(...).map { |uri| uri.model_id.to_i }
end

# Given an array of string GUIDs, return GUID instances
#
# @param ids [Array<String>] GUIDs
# @param type [Class] optionally filter to specific class only
#
# @return [Array<GlobalID>]
def self.local_uris_from_ids(ids, type: ActiveRecord::Base)
ids
.map { |id| GlobalID.parse id }
.select { |uri| (klass = uri.model_name.safe_constantize) && klass <= type }
end

def self.unauthorized_object(error)
raise Exceptions::Forbidden, error.message # Add a top-level error to the response instead of returning nil.
end
Expand Down
74 changes: 66 additions & 8 deletions spec/graphql/gql/zammad_schema_global_id_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,77 @@

RSpec.describe Gql::ZammadSchema, type: :graphql do

it 'generates GraphQL::ID values' do
expect(described_class.id_from_object(Ticket.first)).to eq('gid://zammad/Ticket/1')
describe '.id_from_object' do
it 'generates GraphQL::ID values' do
expect(described_class.id_from_object(Ticket.first)).to eq('gid://zammad/Ticket/1')
end
end

it 'resolves GraphQL::ID values' do
expect(described_class.object_from_id('gid://zammad/Ticket/1')).to eq(Ticket.first)
describe '.object_from_id' do
it 'resolves GraphQL::ID values' do
expect(described_class.object_from_id('gid://zammad/Ticket/1')).to eq(Ticket.first)
end
end

it 'resolves internal IDs to GraphQL::IDs' do
expect(described_class.id_from_internal_id(Ticket, 1)).to eq('gid://zammad/Ticket/1')
describe '.id_from_internal_id' do
it 'resolves internal IDs to GraphQL::IDs' do
expect(described_class.id_from_internal_id(Ticket, 1)).to eq('gid://zammad/Ticket/1')
end

it 'resolves internal IDs to GraphQL::IDs (with class name as string)' do
expect(described_class.id_from_internal_id('Ticket', 1)).to eq('gid://zammad/Ticket/1')
end
end

describe '.internal_id_from_id' do
let(:id) { [*(1..999)].sample }
let(:gid_string) { "gid://zammad/Ticket/#{id}" }

it 'returns internal ID based on given global ID' do
expect(described_class.internal_id_from_id(gid_string)).to eq(id)
end

it 'returns nil if class is not allowed' do
expect(described_class.internal_id_from_id(gid_string, type: User)).to be_nil
end

it 'returns internal ID if class is allowed' do
expect(described_class.internal_id_from_id(gid_string, type: Ticket)).to eq(id)
end
end

describe '.internal_ids_from_ids' do
let(:id) { [*(1..999)].sample }
let(:gid_string) { "gid://zammad/Ticket/#{id}" }

it 'returns internal IDs based on given global IDs' do
expect(described_class.internal_ids_from_ids([gid_string])).to eq([id])
end

it 'returns internal IDs if class is allowed' do
expect(described_class.internal_ids_from_ids([gid_string], type: Ticket)).to eq([id])
end

it 'skips item if class is not allowed' do
expect(described_class.internal_ids_from_ids([gid_string], type: User)).to be_blank
end
end

it 'resolves internal IDs to GraphQL::IDs (with class name as string)' do
expect(described_class.id_from_internal_id('Ticket', 1)).to eq('gid://zammad/Ticket/1')
describe '.local_uris_from_ids' do
let(:id) { [*(1..999)].sample }
let(:gid_string) { "gid://zammad/Ticket/#{id}" }
let(:gid) { GlobalID.new(gid_string) }

it 'returns given global ID as GlobalID' do
expect(described_class.local_uris_from_ids([gid_string])).to eq([gid])
end

it 'returns given global ID if class is allowed' do
expect(described_class.local_uris_from_ids([gid_string], type: Ticket)).to eq([gid])
end

it 'skips item if class is not allowed' do
expect(described_class.local_uris_from_ids([gid_string], type: User)).to be_blank
end
end
end

0 comments on commit 73e55ee

Please sign in to comment.